You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by bm...@apache.org on 2021/01/13 15:07:57 UTC

[shiro] branch SHIRO-290 updated (0a488f9 -> 1e15994)

This is an automated email from the ASF dual-hosted git repository.

bmarwell pushed a change to branch SHIRO-290
in repository https://gitbox.apache.org/repos/asf/shiro.git.


 discard 0a488f9  [SHIRO-290] Implement BCrypt and Argon2
 discard 307c962  (build) add comments, remove empty profile, update jacoco
 discard 5cb30ea  (build) fix JDK 16/17 by replacing some EasyMock tests with Mockito, illegal-access=permit for guice tests. Remove JDK17 for now.
 discard 76a8df7  (no-issue) remove invalid and unused imports leading to a compile error.
 discard cb7d427  Delete .travis.yml
     add b362df3  Delete .travis.yml
     add ab0fede  Merge pull request #276 from apache/bmarwell-delete-travis
     add 2b4796d  (no-issue) remove invalid and unused imports leading to a compile error.
     add 40c959f  Merge pull request #277 from bmarwell/invalidimports
     add eca1f2a  (build) fix JDK 16/17 by replacing some EasyMock tests with Mockito, illegal-access=permit for guice tests. Remove JDK17 for now.
     add cf2d085  Merge pull request #278 from bmarwell/jdk16-17
     add cc1933a  (build) add comments, remove empty profile, update jacoco
     new 1e15994  [SHIRO-290] Implement BCrypt and Argon2

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (0a488f9)
            \
             N -- N -- N   refs/heads/SHIRO-290 (1e15994)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[shiro] 01/01: [SHIRO-290] Implement BCrypt and Argon2

Posted by bm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bmarwell pushed a commit to branch SHIRO-290
in repository https://gitbox.apache.org/repos/asf/shiro.git

commit 1e15994326f6dc901c3b562966e84664fce668e0
Author: Benjamin Marwell <bm...@apache.org>
AuthorDate: Thu Dec 31 10:45:35 2020 +0100

    [SHIRO-290] Implement BCrypt and Argon2
    
    [SHIRO-290] WIP: Implement Unix crypt format, starting with bcrypt.
    
      - TBD: HashRequest
      - TBD: PasswortMatcher doesn’t know about the new hash format yet
    
    [SHIRO-290] Rework to use existing Shiro1CryptFormat.
    
      - Hashes can now compare themselves to a given password.
        Reviwers: Review method placement and HAsh class description.
      - removed hashrequest
      - removed UnixCryptFormat
      - API change: made salt not-nullable.
        Additional constructor is supplied for hashing without or with
        default salt, the former and other methods/fields using
        SimpleByteSource.empty().
        Reviewers: Pay attention to method logic, so no empty salt is
        being used where a former `null` value would have created a
        new, random salt.
      - Modified tests to not expect exceptions in certain cases.
      - Modified tests to not expect passwordService calls when supplying an
        existing hash.
      - TBD: Fix Javadocs
      - TBD: Fix Hasher utility
      - TBD: Deprecate old non-KDF hash classes
    
    [SHIRO-290] Prepare argon2 implementation.
    
     - BCrypt iterations vs cost: make iterations return iterations
     - add validate methods
    
    [SHIRO-290] Implement Argon2Hash.java.
    
     - expand iterations field to take a comma separated list. Maybe just create a Shiro2CryptFormat instead?
     - Hex and Base64 formats are not fixed. Maybe we can drop them?
     - Fixed parameter "algorithm name" not taken into account for bcrypt.
     - Allow Hasher to read from stdin
     - Added a short test for Hasher.java.
     - Changed default DefaultPasswordService.java algorithm to "Argon2id".
    
    [SHIRO-290] Implement Shiro2CryptFormat.java.
    
     - Only fields 1 and two are defined, rest is defined by the hash implementation
     - Therefore fully backwards-compatible to Shiro1CryptFormat.java.
     - Loads formats from ProvidedKdfHashes.java.
       We could also think of a pluggable mechanism, like using service loaders to hide classes
       like OpenBSDBase64.
     - In AbstractCryptHash.java, renamed `version` to `algorithmName`.
     - Removed iterations from AbstractCryptHash.java, they are possibly an implementation detail not
       present in other implementations (like bcrypt).
     - Signature change: `PasswordService.encryptPassword(Object plaintext)` will now throw
       a NullPointerException on `null` parameter. It was never specified how this method would behave.
    
    [SHIRO-290] Add hasher tests
    
     - fix invalid cost factor for bcrypt when input is 0.
     - output Hasher messages using slf4j.
    
    [SHIRO-290] ServiceLoadable KDF algorithms.
    
      - Move BCrypt and Argon2 into their own modules
      - Add a SPI
      - Remove hardcoded parameters, replace with ParameterMap for the hashRequest
    
    [SHIRO-290] implemented review comments
    
     - remove at least MD2, MD5 and Sha1
     - Remove unused support-hashes module
     - changed group and artifact-ids for new modules
     - fixed compilation issue in Hasher (needs more work though)
     - add "since 2.0" comments
    
    [SHIRO-290] add some javadoc, make implementation classes package-private.
    
    [SHIRO-290] doc updates
---
 RELEASE-NOTES                                      |  20 +-
 core/pom.xml                                       |  10 +
 .../shiro/authc/SimpleAuthenticationInfo.java      |  26 +-
 .../authc/credential/DefaultPasswordService.java   |  58 ++--
 .../authc/credential/HashedCredentialsMatcher.java |  32 +-
 .../authc/credential/Md2CredentialsMatcher.java    |  47 ---
 .../authc/credential/Md5CredentialsMatcher.java    |  46 ---
 .../shiro/authc/credential/PasswordMatcher.java    |  18 +-
 .../authc/credential/Sha1CredentialsMatcher.java   |  46 ---
 .../shiro/realm/text/TextConfigurationRealm.java   |   1 +
 .../credential/DefaultPasswordServiceTest.groovy   |  49 ++-
 .../authc/credential/PasswordMatcherTest.groovy    |  49 ++-
 .../credential/HashedCredentialsMatcherTest.java   |  24 +-
 .../credential/Md2CredentialsMatcherTest.java      |  39 ---
 .../credential/Md5CredentialsMatcherTest.java      |  37 ---
 .../credential/Sha1CredentialsMatcherTest.java     |  37 ---
 crypto/cipher/pom.xml                              |   1 -
 crypto/hash/pom.xml                                |   5 +
 .../shiro/crypto/hash/AbstractCryptHash.java       | 253 +++++++++++++++
 .../org/apache/shiro/crypto/hash/AbstractHash.java |  74 +----
 .../shiro/crypto/hash/ConfigurableHashService.java |  34 +-
 .../shiro/crypto/hash/DefaultHashService.java      | 281 ++---------------
 .../java/org/apache/shiro/crypto/hash/Hash.java    |  12 +-
 .../org/apache/shiro/crypto/hash/HashProvider.java |  62 ++++
 .../org/apache/shiro/crypto/hash/HashRequest.java  |  75 ++---
 .../java/org/apache/shiro/crypto/hash/HashSpi.java |  87 +++++
 .../java/org/apache/shiro/crypto/hash/Md2Hash.java |  65 ----
 .../java/org/apache/shiro/crypto/hash/Md5Hash.java |  66 ----
 .../org/apache/shiro/crypto/hash/Sha1Hash.java     |  67 ----
 .../org/apache/shiro/crypto/hash/SimpleHash.java   |  85 ++++-
 .../shiro/crypto/hash/SimpleHashProvider.java      | 219 +++++++++++++
 .../shiro/crypto/hash/SimpleHashRequest.java       |  51 +--
 .../shiro/crypto/hash/format/Base64Format.java     |  14 +-
 .../hash/format/DefaultHashFormatFactory.java      |   5 +-
 .../shiro/crypto/hash/format/HashFormat.java       |   5 +-
 .../apache/shiro/crypto/hash/format/HexFormat.java |  12 +-
 .../crypto/hash/format/ProvidedHashFormat.java     |  13 +-
 .../crypto/hash/format/Shiro1CryptFormat.java      |  18 +-
 .../crypto/hash/format/Shiro2CryptFormat.java      | 143 +++++++++
 crypto/hash/src/main/resources/META-INF/NOTICE     |   2 +-
 .../services/org.apache.shiro.crypto.hash.HashSpi  |  20 ++
 .../crypto/hash/DefaultHashServiceTest.groovy      |  82 +----
 .../crypto/hash/HashRequestBuilderTest.groovy      |  31 +-
 .../crypto/hash/format/Base64FormatTest.groovy     |  10 +-
 .../format/DefaultHashFormatFactoryTest.groovy     |   7 +-
 .../shiro/crypto/hash/format/HexFormatTest.groovy  |  12 +-
 .../hash/format/ProvidedHashFormatTest.groovy      |   3 +-
 .../hash/format/Shiro1CryptFormatTest.groovy       |   7 +-
 crypto/pom.xml                                     |   1 +
 crypto/{cipher => support/hashes/argon2}/pom.xml   |  64 ++--
 .../crypto/support/hashes/argon2/Argon2Hash.java   | 349 +++++++++++++++++++++
 .../support/hashes/argon2/Argon2HashProvider.java  | 207 ++++++++++++
 .../argon2}/src/main/resources/META-INF/NOTICE     |   9 +-
 .../services/org.apache.shiro.crypto.hash.HashSpi  |  20 ++
 .../support/hashes/argon2/Argon2HashTest.groovy    |  88 ++++++
 crypto/{cipher => support/hashes/bcrypt}/pom.xml   |  64 ++--
 .../crypto/support/hashes/bcrypt/BCryptHash.java   | 200 ++++++++++++
 .../support/hashes/bcrypt/BCryptProvider.java      | 144 +++++++++
 .../support/hashes/bcrypt/OpenBSDBase64.java       | 179 +++++++++++
 .../bcrypt}/src/main/resources/META-INF/NOTICE     |   9 +-
 .../services/org.apache.shiro.crypto.hash.HashSpi  |  20 ++
 .../support/hashes/bcrypt/BCryptHashTest.groovy    |  98 ++++++
 crypto/{ => support}/pom.xml                       |  18 +-
 .../org/apache/shiro/lang/codec/CodecSupport.java  |  40 ++-
 .../apache/shiro/lang/util/SimpleByteSource.java   |  13 +-
 .../org/apache/shiro/lang/util/StringUtils.java    |  12 +
 pom.xml                                            |  17 +
 tools/hasher/pom.xml                               |  17 +-
 .../java/org/apache/shiro/tools/hasher/Hasher.java |  70 +++--
 .../hasher/src/main/resources/logback.xml          |  31 +-
 .../org/apache/shiro/tools/hasher/HasherTest.java  | 101 ++++++
 .../hasher/src/test/resources/logback-test.xml     |  28 +-
 72 files changed, 2889 insertions(+), 1270 deletions(-)

diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index 7b39af3..42bf342 100644
--- a/RELEASE-NOTES
+++ b/RELEASE-NOTES
@@ -17,10 +17,28 @@
 
 This is not an official release notes document.  It exists for Shiro developers
 to jot down their notes while working in the source code.  These notes will be
-combined with Jira's auto-generated release notes during a release for the
+combined with Jira’s auto-generated release notes during a release for the
 total set.
 
 ###########################################################
+# 2.0.0
+###########################################################
+
+Improvement
+
+    [SHIRO-290] Implement bcrypt and argon2 KDF algorithms
+
+Backwards Incompatible Changes
+--------------------------------
+
+* Changed default DefaultPasswordService.java algorithm to "Argon2id".
+* PasswordService.encryptPassword(Object plaintext) will now throw a NullPointerException on null parameter.
+  It was never specified how this method would behave.
+* Made salt non-nullable.
+* Removed methods in PasswordMatcher.
+
+
+###########################################################
 # 1.5.3
 ###########################################################
 
diff --git a/core/pom.xml b/core/pom.xml
index 3c247f1..5de40a9 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -102,6 +102,16 @@
             <artifactId>shiro-crypto-hash</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-argon2</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-bcrypt</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-crypto-cipher</artifactId>
         </dependency>
diff --git a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
index 63d3cf5..612d4a9 100644
--- a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
+++ b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java
@@ -18,13 +18,15 @@
  */
 package org.apache.shiro.authc;
 
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
 import org.apache.shiro.subject.MutablePrincipalCollection;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.apache.shiro.subject.SimplePrincipalCollection;
-import org.apache.shiro.lang.util.ByteSource;
 
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Set;
 
 
@@ -37,6 +39,7 @@ import java.util.Set;
  */
 public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, SaltedAuthenticationInfo {
 
+    private static final long serialVersionUID = 5390456512469696779L;
     /**
      * The principals identifying the account associated with this AuthenticationInfo instance.
      */
@@ -51,7 +54,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      *
      * @since 1.1
      */
-    protected ByteSource credentialsSalt;
+    protected ByteSource credentialsSalt = SimpleByteSource.empty();
 
     /**
      * Default no-argument constructor.
@@ -124,6 +127,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
     }
 
 
+    @Override
     public PrincipalCollection getPrincipals() {
         return principals;
     }
@@ -137,6 +141,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
         this.principals = principals;
     }
 
+    @Override
     public Object getCredentials() {
         return credentials;
     }
@@ -163,6 +168,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      *         hashed at all.
      * @since 1.1
      */
+    @Override
     public ByteSource getCredentialsSalt() {
         return credentialsSalt;
     }
@@ -189,6 +195,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      *
      * @param info the <code>AuthenticationInfo</code> to add into this instance.
      */
+    @Override
     @SuppressWarnings("unchecked")
     public void merge(AuthenticationInfo info) {
         if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
@@ -249,14 +256,21 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      * @return <code>true</code> if the Object argument is an <code>instanceof SimpleAuthenticationInfo</code> and
      *         its {@link #getPrincipals() principals} are equal to this instance's principals, <code>false</code> otherwise.
      */
+    @Override
     public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof SimpleAuthenticationInfo)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof SimpleAuthenticationInfo)) {
+            return false;
+        }
 
         SimpleAuthenticationInfo that = (SimpleAuthenticationInfo) o;
 
         //noinspection RedundantIfStatement
-        if (principals != null ? !principals.equals(that.principals) : that.principals != null) return false;
+        if (!Objects.equals(principals, that.principals)) {
+            return false;
+        }
 
         return true;
     }
@@ -266,6 +280,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      *
      * @return the hashcode of the internal {@link #getPrincipals() principals} instance.
      */
+    @Override
     public int hashCode() {
         return (principals != null ? principals.hashCode() : 0);
     }
@@ -275,6 +290,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal
      *
      * @return <code>{@link #getPrincipals() principals}.toString()</code>
      */
+    @Override
     public String toString() {
         return principals.toString();
     }
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java
index ea12668..6c0578f 100644
--- a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java
+++ b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java
@@ -18,32 +18,36 @@
  */
 package org.apache.shiro.authc.credential;
 
-import java.security.MessageDigest;
-
 import org.apache.shiro.crypto.hash.DefaultHashService;
 import org.apache.shiro.crypto.hash.Hash;
 import org.apache.shiro.crypto.hash.HashRequest;
 import org.apache.shiro.crypto.hash.HashService;
-import org.apache.shiro.crypto.hash.format.*;
+import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory;
+import org.apache.shiro.crypto.hash.format.HashFormat;
+import org.apache.shiro.crypto.hash.format.HashFormatFactory;
+import org.apache.shiro.crypto.hash.format.ParsableHashFormat;
+import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat;
 import org.apache.shiro.lang.util.ByteSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.security.MessageDigest;
+
+import static java.util.Objects.requireNonNull;
+
 /**
  * Default implementation of the {@link PasswordService} interface that relies on an internal
  * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function:
  * <h2>Hashing Passwords</h2>
  *
  * <h2>Comparing Passwords</h2>
- * All hashing operations are performed by the internal {@link #getHashService() hashService}.  After the hash
- * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}.
+ * All hashing operations are performed by the internal {@link #getHashService() hashService}.
  *
  * @since 1.2
  */
 public class DefaultPasswordService implements HashingPasswordService {
 
-    public static final String DEFAULT_HASH_ALGORITHM = "SHA-256";
-    public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000
+    public static final String DEFAULT_HASH_ALGORITHM = "argon2id";
 
     private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class);
 
@@ -53,25 +57,33 @@ public class DefaultPasswordService implements HashingPasswordService {
 
     private volatile boolean hashFormatWarned; //used to avoid excessive log noise
 
+    /**
+     * Constructs a new PasswordService with a default hash service and the default
+     * algorithm name {@value #DEFAULT_HASH_ALGORITHM}, a default hash format (shiro2) and
+     * a default hashformat factory.
+     *
+     * <p>The default algorithm can change between minor versions and does not introduce
+     * API incompatibility by design.</p>
+     */
     public DefaultPasswordService() {
         this.hashFormatWarned = false;
 
         DefaultHashService hashService = new DefaultHashService();
-        hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
-        hashService.setHashIterations(DEFAULT_HASH_ITERATIONS);
-        hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure
+        hashService.setDefaultAlgorithmName(DEFAULT_HASH_ALGORITHM);
         this.hashService = hashService;
 
-        this.hashFormat = new Shiro1CryptFormat();
+        this.hashFormat = new Shiro2CryptFormat();
         this.hashFormatFactory = new DefaultHashFormatFactory();
     }
 
+    @Override
     public String encryptPassword(Object plaintext) {
-        Hash hash = hashPassword(plaintext);
+        Hash hash = hashPassword(requireNonNull(plaintext));
         checkHashFormatDurability();
         return this.hashFormat.format(hash);
     }
 
+    @Override
     public Hash hashPassword(Object plaintext) {
         ByteSource plaintextBytes = createByteSource(plaintext);
         if (plaintextBytes == null || plaintextBytes.isEmpty()) {
@@ -81,6 +93,7 @@ public class DefaultPasswordService implements HashingPasswordService {
         return hashService.computeHash(request);
     }
 
+    @Override
     public boolean passwordsMatch(Object plaintext, Hash saved) {
         ByteSource plaintextBytes = createByteSource(plaintext);
 
@@ -92,11 +105,7 @@ public class DefaultPasswordService implements HashingPasswordService {
             }
         }
 
-        HashRequest request = buildHashRequest(plaintextBytes, saved);
-
-        Hash computed = this.hashService.computeHash(request);
-
-        return constantEquals(saved.toString(), computed.toString());
+        return saved.matchesPassword(plaintextBytes);
     }
 
     private boolean constantEquals(String savedHash, String computedHash) {
@@ -133,6 +142,7 @@ public class DefaultPasswordService implements HashingPasswordService {
         return ByteSource.Util.bytes(o);
     }
 
+    @Override
     public boolean passwordsMatch(Object submittedPlaintext, String saved) {
         ByteSource plaintextBytes = createByteSource(submittedPlaintext);
 
@@ -151,9 +161,9 @@ public class DefaultPasswordService implements HashingPasswordService {
         //configuration changes.
         HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved);
 
-        if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) {
+        if (discoveredFormat instanceof ParsableHashFormat) {
 
-            ParsableHashFormat parsableHashFormat = (ParsableHashFormat)discoveredFormat;
+            ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat;
             Hash savedHash = parsableHashFormat.parse(saved);
 
             return passwordsMatch(submittedPlaintext, savedHash);
@@ -174,16 +184,6 @@ public class DefaultPasswordService implements HashingPasswordService {
         return constantEquals(saved, formatted);
     }
 
-    protected HashRequest buildHashRequest(ByteSource plaintext, Hash saved) {
-        //keep everything from the saved hash except for the source:
-        return new HashRequest.Builder().setSource(plaintext)
-                //now use the existing saved data:
-                .setAlgorithmName(saved.getAlgorithmName())
-                .setSalt(saved.getSalt())
-                .setIterations(saved.getIterations())
-                .build();
-    }
-
     public HashService getHashService() {
         return hashService;
     }
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java
index 1377374..5e6b8ad 100644
--- a/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java
+++ b/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java
@@ -21,13 +21,16 @@ package org.apache.shiro.authc.credential;
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.authc.SaltedAuthenticationInfo;
-import org.apache.shiro.lang.codec.Base64;
-import org.apache.shiro.lang.codec.Hex;
 import org.apache.shiro.crypto.hash.AbstractHash;
 import org.apache.shiro.crypto.hash.Hash;
 import org.apache.shiro.crypto.hash.SimpleHash;
+import org.apache.shiro.lang.codec.Base64;
+import org.apache.shiro.lang.codec.Hex;
+import org.apache.shiro.lang.util.SimpleByteSource;
 import org.apache.shiro.lang.util.StringUtils;
 
+import static java.util.Objects.requireNonNull;
+
 /**
  * A {@code HashedCredentialMatcher} provides support for hashing of supplied {@code AuthenticationToken} credentials
  * before being compared to those in the {@code AuthenticationInfo} from the data store.
@@ -49,10 +52,7 @@ import org.apache.shiro.lang.util.StringUtils;
  * and multiple hash iterations.  Please read this excellent
  * <a href="http://www.owasp.org/index.php/Hashing_Java" _target="blank">Hashing Java article</a> to learn about
  * salting and multiple iterations and why you might want to use them. (Note of sections 5
- * &quot;Why add salt?&quot; and 6 "Hardening against the attacker's attack").   We should also note here that all of
- * Shiro's Hash implementations (for example, {@link org.apache.shiro.crypto.hash.Md5Hash Md5Hash},
- * {@link org.apache.shiro.crypto.hash.Sha1Hash Sha1Hash}, etc) support salting and multiple hash iterations via
- * overloaded constructors.
+ * &quot;Why add salt?&quot; and 6 "Hardening against the attacker's attack").</p>
  * <h4>Real World Case Study</h4>
  * In April 2010, some public Atlassian Jira and Confluence
  * installations (Apache Software Foundation, Codehaus, etc) were the target of account attacks and user accounts
@@ -112,8 +112,8 @@ import org.apache.shiro.lang.util.StringUtils;
  * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their
  * supporting {@code CredentialsMatcher} implementations.
  *
- * @see org.apache.shiro.crypto.hash.Md5Hash
- * @see org.apache.shiro.crypto.hash.Sha1Hash
+ * @see org.apache.shiro.crypto.hash.Sha256Hash
+ * @see org.apache.shiro.crypto.hash.Sha384Hash
  * @see org.apache.shiro.crypto.hash.Sha256Hash
  * @since 0.9
  */
@@ -341,6 +341,7 @@ public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
      * @param info the AuthenticationInfo from which to retrieve the credentials which assumed to be in already-hashed form.
      * @return a {@link Hash Hash} instance representing the given AuthenticationInfo's stored credentials.
      */
+    @Override
     protected Object getCredentials(AuthenticationInfo info) {
         Object credentials = info.getCredentials();
 
@@ -400,14 +401,14 @@ public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
      * @since 1.1
      */
     protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
-        Object salt = null;
+        final Object salt;
         if (info instanceof SaltedAuthenticationInfo) {
             salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
-        } else {
+        } else if (isHashSalted()) {
             //retain 1.0 backwards compatibility:
-            if (isHashSalted()) {
-                salt = getSalt(token);
-            }
+            salt = getSalt(token);
+        } else {
+            salt = SimpleByteSource.empty();
         }
         return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
     }
@@ -435,14 +436,15 @@ public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
      * implementation/algorithm used is based on the {@link #getHashAlgorithmName() hashAlgorithmName} property.
      *
      * @param credentials    the submitted authentication token's credentials to hash
-     * @param salt           the value to salt the hash, or {@code null} if a salt will not be used.
+     * @param salt           the value to salt the hash. Cannot be {@code null}, but an empty ByteSource.
      * @param hashIterations the number of times to hash the credentials.  At least one hash will always occur though,
      *                       even if this argument is 0 or negative.
      * @return the hashed value of the provided credentials, according to the specified salt and hash iterations.
+     * @throws NullPointerException if salt is {@code null}.
      */
     protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
         String hashAlgorithmName = assertHashAlgorithmName();
-        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
+        return new SimpleHash(hashAlgorithmName, credentials, requireNonNull(salt, "salt cannot be null."), hashIterations);
     }
 
     /**
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java
deleted file mode 100644
index c968df5..0000000
--- a/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Hash;
-import org.apache.shiro.crypto.hash.Md2Hash;
-
-
-/**
- * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be
- * MD2 hashed.
- * <p/>
- * <b>Note:</b> the MD2, <a href="http://en.wikipedia.org/wiki/MD5">MD5</a> and
- * <a href="http://en.wikipedia.org/wiki/SHA_hash_functions">SHA-1</a> algorithms are now known to be vulnerable to
- * compromise and/or collisions (read the linked pages for more).  While most applications are ok with either of these
- * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their
- * supporting <code>CredentialsMatcher</code> implementations.</p>
- *
- * @since 0.9
- * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its
- *             {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property.
- */
-@Deprecated
-public class Md2CredentialsMatcher extends HashedCredentialsMatcher {
-
-    public Md2CredentialsMatcher() {
-        super();
-        setHashAlgorithmName(Md2Hash.ALGORITHM_NAME);
-    }
-}
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java
deleted file mode 100644
index 81b8f13..0000000
--- a/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Hash;
-import org.apache.shiro.crypto.hash.Md5Hash;
-
-
-/**
- * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be
- * MD5 hashed.
- * <p/>
- * <b>Note:</b> <a href="http://en.wikipedia.org/wiki/MD5">MD5</a> and
- * <a href="http://en.wikipedia.org/wiki/SHA_hash_functions">SHA-1</a> algorithms are now known to be vulnerable to
- * compromise and/or collisions (read the linked pages for more).  While most applications are ok with either of these
- * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their
- * supporting <code>CredentialsMatcher</code> implementations.</p>
- *
- * @since 0.9
- * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its
- *             {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property.
- */
-public class Md5CredentialsMatcher extends HashedCredentialsMatcher {
-
-    public Md5CredentialsMatcher() {
-        super();
-        setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
-    }
-}
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java
index e687dcc..dd60a85 100644
--- a/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java
+++ b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java
@@ -21,6 +21,7 @@ package org.apache.shiro.authc.credential;
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.crypto.hash.Hash;
+import org.apache.shiro.lang.util.ByteSource;
 
 /**
  * A {@link CredentialsMatcher} that employs best-practices comparisons for hashed text passwords.
@@ -39,6 +40,7 @@ public class PasswordMatcher implements CredentialsMatcher {
         this.passwordService = new DefaultPasswordService();
     }
 
+    @Override
     public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
 
         PasswordService service = ensurePasswordService();
@@ -49,23 +51,11 @@ public class PasswordMatcher implements CredentialsMatcher {
 
         if (storedCredentials instanceof Hash) {
             Hash hashedPassword = (Hash)storedCredentials;
-            HashingPasswordService hashingService = assertHashingPasswordService(service);
-            return hashingService.passwordsMatch(submittedPassword, hashedPassword);
+            return hashedPassword.matchesPassword(ByteSource.Util.bytes(submittedPassword));
         }
         //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above):
         String formatted = (String)storedCredentials;
-        return passwordService.passwordsMatch(submittedPassword, formatted);
-    }
-
-    private HashingPasswordService assertHashingPasswordService(PasswordService service) {
-        if (service instanceof HashingPasswordService) {
-            return (HashingPasswordService) service;
-        }
-        String msg = "AuthenticationInfo's stored credentials are a Hash instance, but the " +
-                "configured passwordService is not a " +
-                HashingPasswordService.class.getName() + " instance.  This is required to perform Hash " +
-                "object password comparisons.";
-        throw new IllegalStateException(msg);
+        return service.passwordsMatch(submittedPassword, formatted);
     }
 
     private PasswordService ensurePasswordService() {
diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java
deleted file mode 100644
index 6cdd328..0000000
--- a/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Hash;
-import org.apache.shiro.crypto.hash.Sha1Hash;
-
-
-/**
- * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be
- * SHA hashed.
- * <p/>
- * <b>Note:</b> <a href="http://en.wikipedia.org/wiki/MD5">MD5</a> and
- * <a href="http://en.wikipedia.org/wiki/SHA_hash_functions">SHA-1</a> algorithms are now known to be vulnerable to
- * compromise and/or collisions (read the linked pages for more).  While most applications are ok with either of these
- * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their
- * supporting <code>CredentialsMatcher</code> implementations.</p>
- *
- * @since 0.9
- * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its
- *             {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property.
- */
-public class Sha1CredentialsMatcher extends HashedCredentialsMatcher {
-
-    public Sha1CredentialsMatcher() {
-        super();
-        setHashAlgorithmName(Sha1Hash.ALGORITHM_NAME);
-    }
-}
diff --git a/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java b/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java
index 0439f93..53d16ed 100644
--- a/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java
+++ b/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java
@@ -184,6 +184,7 @@ public class TextConfigurationRealm extends SimpleAccountRealm {
 
             String[] passwordAndRolesArray = StringUtils.split(value);
 
+            // the first token is expected to be the password.
             String password = passwordAndRolesArray[0];
 
             SimpleAccount account = getUser(username);
diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy
index 5365e75..ffd3ad8 100644
--- a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy
+++ b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy
@@ -23,10 +23,12 @@ import org.apache.shiro.crypto.hash.*
 import org.apache.shiro.crypto.hash.format.HashFormatFactory
 import org.apache.shiro.crypto.hash.format.HexFormat
 import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
-import org.junit.Test
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.function.Executable
 
 import static org.easymock.EasyMock.*
-import static org.junit.Assert.*
+import static org.junit.jupiter.api.Assertions.*
 
 /**
  * Unit tests for the {@link DefaultPasswordService} implementation.
@@ -36,41 +38,42 @@ import static org.junit.Assert.*
 class DefaultPasswordServiceTest {
 
     @Test
+    @DisplayName("throws NPE if plaintext is null")
     void testEncryptPasswordWithNullArgument() {
-        def service = new DefaultPasswordService()
-        assertNull service.encryptPassword(null)
+        def service = createSha256Service()
+
+        assertThrows(NullPointerException, { service.encryptPassword(null) } as Executable)
     }
 
     @Test
     void testHashPasswordWithNullArgument() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         assertNull service.hashPassword(null)
     }
 
     @Test
     void testEncryptPasswordDefault() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         def encrypted = service.encryptPassword("12345")
         assertTrue service.passwordsMatch("12345", encrypted)
     }
 
     @Test
     void testEncryptPasswordWithInvalidMatch() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         def encrypted = service.encryptPassword("ABCDEF")
         assertFalse service.passwordsMatch("ABC", encrypted)
     }
 
     @Test
     void testBackwardsCompatibility() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         def encrypted = service.encryptPassword("12345")
         def submitted = "12345"
         assertTrue service.passwordsMatch(submitted, encrypted);
 
         //change some settings:
-        service.hashService.hashAlgorithmName = "MD5"
-        service.hashService.hashIterations = 250000
+        service.hashService.defaultAlgorithmName = "SHA-512"
 
         def encrypted2 = service.encryptPassword(submitted)
 
@@ -81,7 +84,7 @@ class DefaultPasswordServiceTest {
 
     @Test
     void testHashFormatWarned() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         service.hashFormat = new HexFormat()
         assertTrue service.hashFormat instanceof HexFormat
         service.encryptPassword("test")
@@ -90,13 +93,13 @@ class DefaultPasswordServiceTest {
 
     @Test
     void testPasswordsMatchWithNullOrEmpty() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         assertTrue service.passwordsMatch(null, (String) null)
         assertTrue service.passwordsMatch(null, (Hash) null)
         assertTrue service.passwordsMatch("", (String) null)
         assertTrue service.passwordsMatch(null, "")
         assertFalse service.passwordsMatch(null, "12345")
-        assertFalse service.passwordsMatch(null, new Sha1Hash("test"))
+        assertFalse service.passwordsMatch(null, new Sha384Hash("test"))
     }
 
     @Test
@@ -141,19 +144,6 @@ class DefaultPasswordServiceTest {
     }
 
     @Test
-    void testStringComparisonWhenNotUsingAParsableHashFormat() {
-
-        def service = new DefaultPasswordService()
-        service.hashFormat = new HexFormat()
-        //can't use random salts when using HexFormat:
-        service.hashService.generatePublicSalt = false
-
-        def formatted = service.encryptPassword("12345")
-
-        assertTrue service.passwordsMatch("12345", formatted)
-    }
-
-    @Test
     void testTurkishLocal() {
 
         Locale locale = Locale.getDefault();
@@ -162,7 +152,7 @@ class DefaultPasswordServiceTest {
         Locale.setDefault(new Locale("tr", "TR"));
 
         try {
-            PasswordService passwordService = new DefaultPasswordService();
+            PasswordService passwordService = createSha256Service()
             String password = "333";
             String enc = passwordService.encryptPassword(password);
             assertTrue(passwordService.passwordsMatch(password, enc));
@@ -171,4 +161,9 @@ class DefaultPasswordServiceTest {
             Locale.setDefault(locale);
         }
     }
+
+    private static DefaultPasswordService createSha256Service() {
+        def hashService = new DefaultHashService(defaultAlgorithmName: 'SHA-256')
+        new DefaultPasswordService(hashService: hashService)
+    }
 }
diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy
index 59d5530..d900d6f 100644
--- a/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy
+++ b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy
@@ -20,8 +20,12 @@ package org.apache.shiro.authc.credential
 
 import org.apache.shiro.authc.AuthenticationInfo
 import org.apache.shiro.authc.AuthenticationToken
+import org.apache.shiro.authc.SimpleAuthenticationInfo
+import org.apache.shiro.authc.UsernamePasswordToken
 import org.apache.shiro.crypto.hash.Sha256Hash
+import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat
 import org.junit.Test
+import org.junit.jupiter.api.DisplayName
 
 import static org.easymock.EasyMock.*
 import static org.junit.Assert.*
@@ -87,11 +91,7 @@ class PasswordMatcherTest {
         matcher.passwordService = service
         assertSame service, matcher.passwordService
 
-        try {
-            assertTrue matcher.doCredentialsMatch(token, info)
-            fail "matcher should fail since PasswordService is not a HashingPasswordService"
-        } catch (IllegalStateException expected) {
-        }
+        assertTrue matcher.doCredentialsMatch(token, info)
 
         verify token, info, service
     }
@@ -108,8 +108,6 @@ class PasswordMatcherTest {
         expect(token.credentials).andReturn submittedPassword
         expect(info.credentials).andReturn savedPassword
 
-        expect(service.passwordsMatch(submittedPassword, savedPassword)).andReturn true
-
         replay token, info, service
 
         def matcher = new PasswordMatcher()
@@ -175,7 +173,44 @@ class PasswordMatcherTest {
         }
 
         verify token, info, service
+    }
 
+    @Test
+    @DisplayName("test whether shiro2 bcrypt password can be parsed and matched.")
+    void testBCryptPassword() {
+        // given
+        def matcher = new PasswordMatcher();
+        def bcryptPw = '$shiro2$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.'
+        def bcryptHash = new Shiro2CryptFormat().parse(bcryptPw);
+        def plaintext = 'secret#shiro,password;Jo8opech'
+        def principal = "user"
+        def usernamePasswordToken = new UsernamePasswordToken(principal, plaintext)
+        def authenticationInfo = new SimpleAuthenticationInfo(principal, bcryptHash, "inirealm")
+
+        // when
+        def match = matcher.doCredentialsMatch(usernamePasswordToken, authenticationInfo)
+
+        // then
+        assertTrue match
+    }
+
+    @Test
+    @DisplayName("test whether shiro2 argon2 password can be parsed and matched.")
+    void testArgon2Password() {
+        // given
+        def matcher = new PasswordMatcher();
+        def bcryptPw = '$shiro2$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI'
+        def bcryptHash = new Shiro2CryptFormat().parse(bcryptPw);
+        def plaintext = 'secret#shiro,password;Jo8opech'
+        def principal = "user"
+        def usernamePasswordToken = new UsernamePasswordToken(principal, plaintext)
+        def authenticationInfo = new SimpleAuthenticationInfo(principal, bcryptHash, "inirealm")
+
+        // when
+        def match = matcher.doCredentialsMatch(usernamePasswordToken, authenticationInfo)
+
+        // then
+        assertTrue match
     }
 
 }
diff --git a/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java
index 6c9891f..100a9c8 100644
--- a/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java
+++ b/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java
@@ -23,10 +23,10 @@ import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.authc.SimpleAuthenticationInfo;
 import org.apache.shiro.authc.UsernamePasswordToken;
 import org.apache.shiro.crypto.SecureRandomNumberGenerator;
-import org.apache.shiro.crypto.hash.Sha1Hash;
+import org.apache.shiro.crypto.hash.Sha512Hash;
+import org.apache.shiro.lang.util.ByteSource;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.apache.shiro.subject.SimplePrincipalCollection;
-import org.apache.shiro.lang.util.ByteSource;
 import org.junit.Test;
 
 import static org.junit.Assert.assertTrue;
@@ -43,11 +43,11 @@ public class HashedCredentialsMatcherTest {
     @Test
     public void testSaltedAuthenticationInfo() {
         //use SHA-1 hashing in this test:
-        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME);
+        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME);
 
         //simulate a user account with a SHA-1 hashed and salted password:
         ByteSource salt = new SecureRandomNumberGenerator().nextBytes();
-        Object hashedPassword = new Sha1Hash("password", salt);
+        Object hashedPassword = new Sha512Hash("password", salt);
         SimpleAuthenticationInfo account = new SimpleAuthenticationInfo("username", hashedPassword, salt, "realmName");
 
         //simulate a username/password (plaintext) token created in response to a login attempt:
@@ -63,17 +63,21 @@ public class HashedCredentialsMatcherTest {
      */
     @Test
     public void testBackwardsCompatibleUnsaltedAuthenticationInfo() {
-        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME);
+        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME);
 
         //simulate an account with SHA-1 hashed password (no salt)
         final String username = "username";
         final String password = "password";
-        final Object hashedPassword = new Sha1Hash(password).getBytes();
+        final Object hashedPassword = new Sha512Hash(password).getBytes();
         AuthenticationInfo account = new AuthenticationInfo() {
+            private static final long serialVersionUID = -3613684957517438801L;
+
+            @Override
             public PrincipalCollection getPrincipals() {
                 return new SimplePrincipalCollection(username, "realmName");
             }
 
+            @Override
             public Object getCredentials() {
                 return hashedPassword;
             }
@@ -92,7 +96,7 @@ public class HashedCredentialsMatcherTest {
      */
     @Test
     public void testBackwardsCompatibleSaltedAuthenticationInfo() {
-        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME);
+        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME);
         //enable this for Shiro 1.0 backwards compatibility:
         matcher.setHashSalted(true);
 
@@ -100,12 +104,16 @@ public class HashedCredentialsMatcherTest {
         //(BAD IDEA, but backwards-compatible):
         final String username = "username";
         final String password = "password";
-        final Object hashedPassword = new Sha1Hash(password, username).getBytes();
+        final Object hashedPassword = new Sha512Hash(password, username).getBytes();
         AuthenticationInfo account = new AuthenticationInfo() {
+            private static final long serialVersionUID = -6942549615727484358L;
+
+            @Override
             public PrincipalCollection getPrincipals() {
                 return new SimplePrincipalCollection(username, "realmName");
             }
 
+            @Override
             public Object getCredentials() {
                 return hashedPassword;
             }
diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java
deleted file mode 100644
index 5286a58..0000000
--- a/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Md2Hash;
-
-
-/**
- * @since Jun 10, 2008 4:38:16 PM
- */
-public class Md2CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest {
-
-    public Class<? extends HashedCredentialsMatcher> getMatcherClass() {
-        return Md2CredentialsMatcher.class;
-    }
-
-    public AbstractHash hash(Object credentials) {
-        return new Md2Hash(credentials);
-    }
-}
-
-
diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java
deleted file mode 100644
index 4c9d71d..0000000
--- a/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Md5Hash;
-
-
-/**
- * @since Jun 10, 2008 4:59:36 PM
- */
-public class Md5CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest {
-
-    public Class<? extends HashedCredentialsMatcher> getMatcherClass() {
-        return Md5CredentialsMatcher.class;
-    }
-
-    public AbstractHash hash(Object credentials) {
-        return new Md5Hash(credentials);
-    }
-}
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java
deleted file mode 100644
index 29d6283..0000000
--- a/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.authc.credential;
-
-import org.apache.shiro.crypto.hash.AbstractHash;
-import org.apache.shiro.crypto.hash.Sha1Hash;
-
-
-/**
- * @since Jun 10, 2008 5:00:30 PM
- */
-public class Sha1CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest {
-
-    public Class<? extends HashedCredentialsMatcher> getMatcherClass() {
-        return Sha1CredentialsMatcher.class;
-    }
-
-    public AbstractHash hash(Object credentials) {
-        return new Sha1Hash(credentials);
-    }
-}
diff --git a/crypto/cipher/pom.xml b/crypto/cipher/pom.xml
index 72974d4..2b03bfd 100644
--- a/crypto/cipher/pom.xml
+++ b/crypto/cipher/pom.xml
@@ -65,7 +65,6 @@
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.64</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/crypto/hash/pom.xml b/crypto/hash/pom.xml
index 6526345..e5503af 100644
--- a/crypto/hash/pom.xml
+++ b/crypto/hash/pom.xml
@@ -61,6 +61,11 @@
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-crypto-core</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java
new file mode 100644
index 0000000..f056c61
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.hash;
+
+import org.apache.shiro.lang.codec.Base64;
+import org.apache.shiro.lang.codec.Hex;
+import org.apache.shiro.lang.util.ByteSource;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.regex.Pattern;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Abstract class for hashes following the posix crypt(3) format.
+ *
+ * <p>These implementations must contain a salt, a salt length, can format themselves to a valid String
+ * suitable for the {@code /etc/shadow} file.</p>
+ *
+ * <p>It also defines the hex and base64 output by wrapping the output of {@link #formatToCryptString()}.</p>
+ *
+ * <p>Implementation notice: Implementations should provide a static {@code fromString()} method.</p>
+ *
+ * @since 2.0
+ */
+public abstract class AbstractCryptHash implements Hash, Serializable {
+
+    private static final long serialVersionUID = 2483214646921027859L;
+
+    protected static final Pattern DELIMITER = Pattern.compile("\\$");
+
+    private final String algorithmName;
+    private final byte[] hashedData;
+    private final ByteSource salt;
+
+    /**
+     * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead.
+     */
+    private String hexEncoded;
+    /**
+     * Cached value of the {@link #toBase64() toBase64()} call so multiple calls won't incur repeated overhead.
+     */
+    private String base64Encoded;
+
+    /**
+     * Constructs an {@link AbstractCryptHash} using the algorithm name, hashed data and salt parameters.
+     *
+     * <p>Other required parameters must be stored by the implementation.</p>
+     *
+     * @param algorithmName internal algorithm name, e.g. {@code 2y} for bcrypt and {@code argon2id} for argon2.
+     * @param hashedData the hashed data as a byte array. Does not include the salt or other parameters.
+     * @param salt the salt which was used when generating the hash.
+     * @throws IllegalArgumentException if the salt is not the same size as {@link #getSaltLength()}.
+     */
+    public AbstractCryptHash(final String algorithmName, final byte[] hashedData, final ByteSource salt) {
+        this.algorithmName = algorithmName;
+        this.hashedData = Arrays.copyOf(hashedData, hashedData.length);
+        this.salt = requireNonNull(salt);
+        checkValid();
+    }
+
+    protected final void checkValid() {
+        checkValidAlgorithm();
+
+        checkValidSalt();
+    }
+
+    /**
+     * Algorithm-specific checks of the algorithm’s parameters.
+     *
+     * <p>While the salt length will be checked by default, other checks will be useful.
+     * Examples are: Argon2 checking for the memory and parallelism parameters, bcrypt checking
+     * for the cost parameters being in a valid range.</p>
+     *
+     * @throws IllegalArgumentException if any of the parameters are invalid.
+     */
+    protected abstract void checkValidAlgorithm();
+
+    /**
+     * Default check method for a valid salt. Can be overridden, because multiple salt lengths could be valid.
+     *
+     * By default, this method checks if the number of bytes in the salt
+     * are equal to the int returned by {@link #getSaltLength()}.
+     *
+     * @throws IllegalArgumentException if the salt length does not match the returned value of {@link #getSaltLength()}.
+     */
+    protected void checkValidSalt() {
+        int length = salt.getBytes().length;
+        if (length != getSaltLength()) {
+            String message = String.format(
+                    Locale.ENGLISH,
+                    "Salt length is expected to be [%d] bytes, but was [%d] bytes.",
+                    getSaltLength(),
+                    length
+            );
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Implemented by subclasses, this specifies the KDF algorithm name
+     * to use when performing the hash.
+     *
+     * <p>When multiple algorithm names are acceptable, then this method should return the primary algorithm name.</p>
+     *
+     * <p>Example: Bcrypt hashed can be identified by {@code 2y} and {@code 2a}. The method will return {@code 2y}
+     * for newly generated hashes by default, unless otherwise overridden.</p>
+     *
+     * @return the KDF algorithm name to use when performing the hash.
+     */
+    @Override
+    public String getAlgorithmName() {
+        return this.algorithmName;
+    }
+
+    /**
+     * The length in number of bytes of the salt which is needed for this algorithm.
+     *
+     * @return the expected length of the salt (in bytes).
+     */
+    public abstract int getSaltLength();
+
+    @Override
+    public ByteSource getSalt() {
+        return this.salt;
+    }
+
+    /**
+     * Returns only the hashed data. Those are of no value on their own. If you need to serialize
+     * the hash, please refer to {@link #formatToCryptString()}.
+     *
+     * @return A copy of the hashed data as bytes.
+     * @see #formatToCryptString()
+     */
+    @Override
+    public byte[] getBytes() {
+        return Arrays.copyOf(this.hashedData, this.hashedData.length);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    /**
+     * Returns a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
+     * <p/>
+     * This implementation caches the resulting hex string so multiple calls to this method remain efficient.
+     *
+     * @return a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
+     */
+    @Override
+    public String toHex() {
+        if (this.hexEncoded == null) {
+            this.hexEncoded = Hex.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8));
+        }
+        return this.hexEncoded;
+    }
+
+    /**
+     * Returns a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
+     * <p/>
+     * This implementation caches the resulting Base64 string so multiple calls to this method remain efficient.
+     *
+     * @return a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
+     */
+    @Override
+    public String toBase64() {
+        if (this.base64Encoded == null) {
+            //cache result in case this method is called multiple times.
+            this.base64Encoded = Base64.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8));
+        }
+        return this.base64Encoded;
+    }
+
+    /**
+     * This method <strong>MUST</strong> return a single-lined string which would also be recognizable by
+     * a posix {@code /etc/passwd} file.
+     *
+     * @return a formatted string, e.g. {@code $2y$10$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.} for bcrypt.
+     */
+    public abstract String formatToCryptString();
+
+    /**
+     * Returns {@code true} if the specified object is an AbstractCryptHash and its
+     * {@link #formatToCryptString()} formatted output} is identical to
+     * this AbstractCryptHash's formatted output, {@code false} otherwise.
+     *
+     * @param other the object (AbstractCryptHash) to check for equality.
+     * @return {@code true} if the specified object is a AbstractCryptHash
+     * and its {@link #formatToCryptString()} formatted output} is identical to
+     * this AbstractCryptHash's formatted output, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other instanceof AbstractCryptHash) {
+            final AbstractCryptHash that = (AbstractCryptHash) other;
+            return this.formatToCryptString().equals(that.formatToCryptString());
+        }
+        return false;
+    }
+
+    /**
+     * Hashes the formatted crypt string.
+     *
+     * <p>Implementations should not override this method, as different algorithms produce different output formats
+     * and require different parameters.</p>
+     * @return a hashcode from the {@link #formatToCryptString() formatted output}.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.formatToCryptString());
+    }
+
+    /**
+     * Simple implementation that merely returns {@link #toHex() toHex()}.
+     *
+     * @return the {@link #toHex() toHex()} value.
+     */
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", AbstractCryptHash.class.getSimpleName() + "[", "]")
+                .add("super=" + super.toString())
+                .add("algorithmName='" + algorithmName + "'")
+                .add("hashedData=" + Arrays.toString(hashedData))
+                .add("salt=" + salt)
+                .add("hexEncoded='" + hexEncoded + "'")
+                .add("base64Encoded='" + base64Encoded + "'")
+                .toString();
+    }
+}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java
index 4bf8373..684647c 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java
@@ -18,11 +18,11 @@
  */
 package org.apache.shiro.crypto.hash;
 
+import org.apache.shiro.crypto.UnknownAlgorithmException;
 import org.apache.shiro.lang.codec.Base64;
 import org.apache.shiro.lang.codec.CodecException;
 import org.apache.shiro.lang.codec.CodecSupport;
 import org.apache.shiro.lang.codec.Hex;
-import org.apache.shiro.crypto.UnknownAlgorithmException;
 
 import java.io.Serializable;
 import java.security.MessageDigest;
@@ -46,6 +46,7 @@ import java.util.Arrays;
 @Deprecated
 public abstract class AbstractHash extends CodecSupport implements Hash, Serializable {
 
+    private static final long serialVersionUID = -4723044219611288405L;
     /**
      * The hashed data
      */
@@ -142,8 +143,10 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      *
      * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash.
      */
+    @Override
     public abstract String getAlgorithmName();
 
+    @Override
     public byte[] getBytes() {
         return this.bytes;
     }
@@ -233,6 +236,7 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      *
      * @return a hex-encoded string of the underlying {@link #getBytes byte array}.
      */
+    @Override
     public String toHex() {
         if (this.hexEncoded == null) {
             this.hexEncoded = Hex.encodeToString(getBytes());
@@ -249,6 +253,7 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      *
      * @return a Base64-encoded string of the underlying {@link #getBytes byte array}.
      */
+    @Override
     public String toBase64() {
         if (this.base64Encoded == null) {
             //cache result in case this method is called multiple times.
@@ -262,6 +267,7 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      *
      * @return the {@link #toHex() toHex()} value.
      */
+    @Override
     public String toString() {
         return toHex();
     }
@@ -274,6 +280,7 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      * @return {@code true} if the specified object is a Hash and its {@link #getBytes byte array} is identical to
      *         this Hash's byte array, {@code false} otherwise.
      */
+    @Override
     public boolean equals(Object o) {
         if (o instanceof Hash) {
             Hash other = (Hash) o;
@@ -287,6 +294,7 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
      *
      * @return toHex().hashCode()
      */
+    @Override
     public int hashCode() {
         if (this.bytes == null || this.bytes.length == 0) {
             return 0;
@@ -294,68 +302,4 @@ public abstract class AbstractHash extends CodecSupport implements Hash, Seriali
         return Arrays.hashCode(this.bytes);
     }
 
-    private static void printMainUsage(Class<? extends AbstractHash> clazz, String type) {
-        System.out.println("Prints an " + type + " hash value.");
-        System.out.println("Usage: java " + clazz.getName() + " [-base64] [-salt <saltValue>] [-times <N>] <valueToHash>");
-        System.out.println("Options:");
-        System.out.println("\t-base64\t\tPrints the hash value as a base64 String instead of the default hex.");
-        System.out.println("\t-salt\t\tSalts the hash with the specified <saltValue>");
-        System.out.println("\t-times\t\tHashes the input <N> number of times");
-    }
-
-    private static boolean isReserved(String arg) {
-        return "-base64".equals(arg) || "-times".equals(arg) || "-salt".equals(arg);
-    }
-
-    static int doMain(Class<? extends AbstractHash> clazz, String[] args) {
-        String simple = clazz.getSimpleName();
-        int index = simple.indexOf("Hash");
-        String type = simple.substring(0, index).toUpperCase();
-
-        if (args == null || args.length < 1 || args.length > 7) {
-            printMainUsage(clazz, type);
-            return -1;
-        }
-        boolean hex = true;
-        String salt = null;
-        int times = 1;
-        String text = args[args.length - 1];
-        for (int i = 0; i < args.length; i++) {
-            String arg = args[i];
-            if (arg.equals("-base64")) {
-                hex = false;
-            } else if (arg.equals("-salt")) {
-                if ((i + 1) >= (args.length - 1)) {
-                    String msg = "Salt argument must be followed by a salt value.  The final argument is " +
-                            "reserved for the value to hash.";
-                    System.out.println(msg);
-                    printMainUsage(clazz, type);
-                    return -1;
-                }
-                salt = args[i + 1];
-            } else if (arg.equals("-times")) {
-                if ((i + 1) >= (args.length - 1)) {
-                    String msg = "Times argument must be followed by an integer value.  The final argument is " +
-                            "reserved for the value to hash";
-                    System.out.println(msg);
-                    printMainUsage(clazz, type);
-                    return -1;
-                }
-                try {
-                    times = Integer.valueOf(args[i + 1]);
-                } catch (NumberFormatException e) {
-                    String msg = "Times argument must be followed by an integer value.";
-                    System.out.println(msg);
-                    printMainUsage(clazz, type);
-                    return -1;
-                }
-            }
-        }
-
-        Hash hash = new Md2Hash(text, salt, times);
-        String hashed = hex ? hash.toHex() : hash.toBase64();
-        System.out.print(hex ? "Hex: " : "Base64: ");
-        System.out.println(hashed);
-        return 0;
-    }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java
index fd7883f..6e4dca5 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java
@@ -18,9 +18,6 @@
  */
 package org.apache.shiro.crypto.hash;
 
-import org.apache.shiro.crypto.RandomNumberGenerator;
-import org.apache.shiro.lang.util.ByteSource;
-
 /**
  * A {@code HashService} that allows configuration of its strategy via JavaBeans-compatible setter methods.
  *
@@ -29,33 +26,12 @@ import org.apache.shiro.lang.util.ByteSource;
 public interface ConfigurableHashService extends HashService {
 
     /**
-     * Sets the 'private' (internal) salt to be paired with a 'public' (random or supplied) salt during hash computation.
-     *
-     * @param privateSalt the 'private' internal salt to be paired with a 'public' (random or supplied) salt during
-     *                    hash computation.
-     */
-    void setPrivateSalt(ByteSource privateSalt);
-
-    /**
-     * Sets the number of hash iterations that will be performed during hash computation.
-     *
-     * @param iterations the number of hash iterations that will be performed during hash computation.
-     */
-    void setHashIterations(int iterations);
-
-    /**
-     * Sets the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute
-     * hashes.
+     * Sets the name of the key derivation function algorithm that will be used to compute
+     * secure hashes for passwords.
      *
-     * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to
-     *             compute hashes.
+     * @param name the name of the key derivation function algorithm that will be used to
+     *             compute secure hashes for passwords.
      */
-    void setHashAlgorithmName(String name);
+    void setDefaultAlgorithmName(String name);
 
-    /**
-     * Sets a source of randomness used to generate public salts that will in turn be used during hash computation.
-     *
-     * @param rng a source of randomness used to generate public salts that will in turn be used during hash computation.
-     */
-    void setRandomNumberGenerator(RandomNumberGenerator rng);
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java
index 486e19d..ed2653f 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java
@@ -18,39 +18,19 @@
  */
 package org.apache.shiro.crypto.hash;
 
-import org.apache.shiro.crypto.RandomNumberGenerator;
-import org.apache.shiro.crypto.SecureRandomNumberGenerator;
-import org.apache.shiro.lang.util.ByteSource;
+import java.security.SecureRandom;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Random;
 
 /**
- * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name,
- * secure-random salt generation, multiple hash iterations and an optional internal
- * {@link #setPrivateSalt(ByteSource) privateSalt}.
+ * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name.
  * <h2>Hash Algorithm</h2>
- * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property.  Any algorithm name
+ * You may specify a hash algorithm via the {@link #setDefaultAlgorithmName(String)} property. Any algorithm name
  * understood by the JDK
  * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
- * will work.  The default is {@code SHA-512}.
- * <h2>Random Salts</h2>
- * When a salt is not specified in a request, this implementation generates secure random salts via its
- * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property.
- * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong
- * salting strategy, as salts should ideally never be based on known/guessable data.  The default instance is a
- * {@link SecureRandomNumberGenerator}.
- * <h2>Hash Iterations</h2>
- * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process.  This technique
- * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would
- * take for an attacker to compromise a password.  This
- * <a href="http://www.stormpath.com/blog/strong-password-hashing-apache-shiro">blog article</a>
- * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'.
- * <p/>
- * You may set the number of hash iterations via the {@link #setHashIterations(int)} property.  The default is
- * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password
- * hashing. See the linked blog article for more info.
- * <h2>Private Salt</h2>
- * If using this implementation as part of a password hashing strategy, it might be desirable to configure a
- * {@link #setPrivateSalt(ByteSource) private salt}:
- * <p/>
+ * will work, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}.
+ * </p>
  * A hash and the salt used to compute it are often stored together.  If an attacker is ever able to access
  * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary
  * to try to brute-force crack the hash (source + complete salt).
@@ -58,60 +38,28 @@ import org.apache.shiro.lang.util.ByteSource;
  * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is
  * <em>much</em> harder to crack the hash value since the attacker does not have the complete inputs necessary.
  * <p/>
- * The {@link #getPrivateSalt() privateSalt} property exists to satisfy this private-and-not-shared part of the salt.
- * If you configure this attribute, you can obtain this additional very important safety feature.
- * <p/>
- * <b>*</b>By default, the {@link #getPrivateSalt() privateSalt} is null, since a sensible default cannot be used that
- * isn't easily compromised (because Shiro is an open-source project and any default could be easily seen and used).
  *
  * @since 1.2
  */
 public class DefaultHashService implements ConfigurableHashService {
 
-    /**
-     * The RandomNumberGenerator to use to randomly generate the public part of the hash salt.
-     */
-    private RandomNumberGenerator rng;
+    private final Random random;
 
     /**
      * The MessageDigest name of the hash algorithm to use for computing hashes.
      */
-    private String algorithmName;
-
-    /**
-     * The 'private' part of the hash salt.
-     */
-    private ByteSource privateSalt;
-
-    /**
-     * The number of hash iterations to perform when computing hashes.
-     */
-    private int iterations;
+    private String defaultAlgorithmName;
 
-    /**
-     * Whether or not to generate public salts if a request does not provide one.
-     */
-    private boolean generatePublicSalt;
 
     /**
      * Constructs a new {@code DefaultHashService} instance with the following defaults:
      * <ul>
-     * <li>{@link #setHashAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
-     * <li>{@link #setHashIterations(int) hashIterations} = {@code 1}</li>
-     * <li>{@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} =
-     * new {@link SecureRandomNumberGenerator}()</li>
-     * <li>{@link #setGeneratePublicSalt(boolean) generatePublicSalt} = {@code false}</li>
+     * <li>{@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
      * </ul>
-     * <p/>
-     * If this hashService will be used for password hashing it is recommended to set the
-     * {@link #setPrivateSalt(ByteSource) privateSalt} and significantly increase the number of
-     * {@link #setHashIterations(int) hashIterations}.  See the class-level JavaDoc for more information.
      */
     public DefaultHashService() {
-        this.algorithmName = "SHA-512";
-        this.iterations = 1;
-        this.generatePublicSalt = false;
-        this.rng = new SecureRandomNumberGenerator();
+        this.random = new SecureRandom();
+        this.defaultAlgorithmName = "argon2";
     }
 
     /**
@@ -123,222 +71,45 @@ public class DefaultHashService implements ConfigurableHashService {
      * <p/>
      * A salt will be generated and used to compute the hash.  The salt is generated as follows:
      * <ol>
-     * <li>Use the {@link #getRandomNumberGenerator() randomNumberGenerator} to generate a new random number.</li>
-     * <li>{@link #combine(ByteSource, ByteSource) combine} this random salt with any configured
-     * {@link #getPrivateSalt() privateSalt}
-     * </li>
      * <li>Use the combined value as the salt used during hash computation</li>
      * </ol>
      * </li>
      * <li>
-     * If the request salt is not null:
-     * <p/>
-     * This indicates that the hash computation is for comparison purposes (of a
-     * previously computed hash).  The request salt will be {@link #combine(ByteSource, ByteSource) combined} with any
-     * configured {@link #getPrivateSalt() privateSalt} and used as the complete salt during hash computation.
-     * </li>
-     * </ul>
-     * <p/>
-     * The returned {@code Hash}'s {@link Hash#getSalt() salt} property
-     * will contain <em>only</em> the 'public' part of the salt and <em>NOT</em> the privateSalt.  See the class-level
-     * JavaDoc explanation for more info.
      *
      * @param request the request to process
      * @return the response containing the result of the hash computation, as well as any hash salt used that should be
      *         exposed to the caller.
      */
+    @Override
     public Hash computeHash(HashRequest request) {
         if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
             return null;
         }
 
         String algorithmName = getAlgorithmName(request);
-        ByteSource source = request.getSource();
-        int iterations = getIterations(request);
-
-        ByteSource publicSalt = getPublicSalt(request);
-        ByteSource privateSalt = getPrivateSalt();
-        ByteSource salt = combine(privateSalt, publicSalt);
 
-        Hash computed = new SimpleHash(algorithmName, source, salt, iterations);
+        Optional<HashSpi> kdfHash = HashProvider.getByAlgorithmName(algorithmName);
+        if (kdfHash.isPresent()) {
+            HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new);
 
-        SimpleHash result = new SimpleHash(algorithmName);
-        result.setBytes(computed.getBytes());
-        result.setIterations(iterations);
-        //Only expose the public salt - not the real/combined salt that might have been used:
-        result.setSalt(publicSalt);
-
-        return result;
-    }
-
-    protected String getAlgorithmName(HashRequest request) {
-        String name = request.getAlgorithmName();
-        if (name == null) {
-            name = getHashAlgorithmName();
+            return hashSpi.newHashFactory(random).generate(request);
         }
-        return name;
-    }
 
-    protected int getIterations(HashRequest request) {
-        int iterations = Math.max(0, request.getIterations());
-        if (iterations < 1) {
-            iterations = Math.max(1, getHashIterations());
-        }
-        return iterations;
+        throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName);
     }
 
-    /**
-     * Returns the public salt that should be used to compute a hash based on the specified request or
-     * {@code null} if no public salt should be used.
-     * <p/>
-     * This implementation functions as follows:
-     * <ol>
-     * <li>If the request salt is not null and non-empty, this will be used, return it.</li>
-     * <li>If the request salt is null or empty:
-     * <ol>
-     * <li>If a private salt has been set <em>OR</em> {@link #isGeneratePublicSalt()} is {@code true},
-     * auto generate a random public salt via the configured
-     * {@link #getRandomNumberGenerator() randomNumberGenerator}.</li>
-     * <li>If a private salt has not been configured and {@link #isGeneratePublicSalt()} is {@code false},
-     * do nothing - return {@code null} to indicate a salt should not be used during hash computation.</li>
-     * </ol>
-     * </li>
-     * </ol>
-     *
-     * @param request request the request to process
-     * @return the public salt that should be used to compute a hash based on the specified request or
-     *         {@code null} if no public salt should be used.
-     */
-    protected ByteSource getPublicSalt(HashRequest request) {
-
-        ByteSource publicSalt = request.getSalt();
-
-        if (publicSalt != null && !publicSalt.isEmpty()) {
-            //a public salt was explicitly requested to be used - go ahead and use it:
-            return publicSalt;
-        }
-
-        publicSalt = null;
-
-        //check to see if we need to generate one:
-        ByteSource privateSalt = getPrivateSalt();
-        boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty();
-
-        //If a private salt exists, we must generate a public salt to protect the integrity of the private salt.
-        //Or generate it if the instance is explicitly configured to do so:
-        if (privateSaltExists || isGeneratePublicSalt()) {
-            publicSalt = getRandomNumberGenerator().nextBytes();
-        }
 
-        return publicSalt;
-    }
-
-    /**
-     * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the
-     * total salt during hash computation.  {@code privateSaltBytes} will be {@code null} }if no private salt has been
-     * configured.
-     *
-     * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes
-     * @param publicSalt  the extra bytes to use in addition to the given private salt.
-     * @return a combination of the specified private salt bytes and extra bytes that will be used as the total
-     *         salt during hash computation.
-     */
-    protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) {
-
-        byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null;
-        int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0;
-
-        byte[] publicSaltBytes = publicSalt != null ? publicSalt.getBytes() : null;
-        int extraBytesLength = publicSaltBytes != null ? publicSaltBytes.length : 0;
-
-        int length = privateSaltLength + extraBytesLength;
-
-        if (length <= 0) {
-            return null;
-        }
-
-        byte[] combined = new byte[length];
-
-        int i = 0;
-        for (int j = 0; j < privateSaltLength; j++) {
-            assert privateSaltBytes != null;
-            combined[i++] = privateSaltBytes[j];
-        }
-        for (int j = 0; j < extraBytesLength; j++) {
-            assert publicSaltBytes != null;
-            combined[i++] = publicSaltBytes[j];
-        }
-
-        return ByteSource.Util.bytes(combined);
-    }
-
-    public void setHashAlgorithmName(String name) {
-        this.algorithmName = name;
-    }
-
-    public String getHashAlgorithmName() {
-        return this.algorithmName;
-    }
-
-    public void setPrivateSalt(ByteSource privateSalt) {
-        this.privateSalt = privateSalt;
-    }
-
-    public ByteSource getPrivateSalt() {
-        return this.privateSalt;
-    }
-
-    public void setHashIterations(int count) {
-        this.iterations = count;
-    }
-
-    public int getHashIterations() {
-        return this.iterations;
+    protected String getAlgorithmName(HashRequest request) {
+        return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName);
     }
 
-    public void setRandomNumberGenerator(RandomNumberGenerator rng) {
-        this.rng = rng;
+    @Override
+    public void setDefaultAlgorithmName(String name) {
+        this.defaultAlgorithmName = name;
     }
 
-    public RandomNumberGenerator getRandomNumberGenerator() {
-        return this.rng;
+    public String getDefaultAlgorithmName() {
+        return this.defaultAlgorithmName;
     }
 
-    /**
-     * Returns {@code true} if a public salt should be randomly generated and used to compute a hash if a
-     * {@link HashRequest} does not specify a salt, {@code false} otherwise.
-     * <p/>
-     * The default value is {@code false} but should definitely be set to {@code true} if the
-     * {@code HashService} instance is being used for password hashing.
-     * <p/>
-     * <b>NOTE:</b> this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured.  If a
-     * private salt has been configured and a request does not provide a salt, a random salt will always be generated
-     * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is,
-     * which is undesirable).
-     *
-     * @return {@code true} if a public salt should be randomly generated and used to compute a hash if a
-     *         {@link HashRequest} does not specify a salt, {@code false} otherwise.
-     */
-    public boolean isGeneratePublicSalt() {
-        return generatePublicSalt;
-    }
-
-    /**
-     * Sets whether or not a public salt should be randomly generated and used to compute a hash if a
-     * {@link HashRequest} does not specify a salt.
-     * <p/>
-     * The default value is {@code false} but should definitely be set to {@code true} if the
-     * {@code HashService} instance is being used for password hashing.
-     * <p/>
-     * <b>NOTE:</b> this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured.  If a
-     * private salt has been configured and a request does not provide a salt, a random salt will always be generated
-     * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is,
-     * which is undesirable).
-     *
-     * @param generatePublicSalt whether or not a public salt should be randomly generated and used to compute a hash
-     *                           if a {@link HashRequest} does not specify a salt.
-     */
-    public void setGeneratePublicSalt(boolean generatePublicSalt) {
-        this.generatePublicSalt = generatePublicSalt;
-    }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java
index 3e26928..ce52ce8 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java
@@ -28,9 +28,6 @@ import org.apache.shiro.lang.util.ByteSource;
  * The bytes returned by the parent interface's {@link #getBytes() getBytes()} are the hashed value of the
  * original input source, also known as the 'checksum' or 'digest'.
  *
- * @see Md2Hash
- * @see Md5Hash
- * @see Sha1Hash
  * @see Sha256Hash
  * @see Sha384Hash
  * @see Sha512Hash
@@ -64,4 +61,13 @@ public interface Hash extends ByteSource {
      */
     int getIterations();
 
+    /**
+     * Tests if a given passwords matches with this instance.
+     *
+     * <p>Usually implementations will re-create {@code this} but with the given plaintext bytes as secret.</p>
+     *
+     * @param plaintextBytes the plaintext bytes from a user.
+     * @return {@code true} if the given plaintext generates an equal hash with the same parameters as from this hash.
+     */
+    boolean matchesPassword(ByteSource plaintextBytes);
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java
new file mode 100644
index 0000000..64de6f9
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.hash;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.stream.StreamSupport;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Hashes used by the Shiro2CryptFormat class.
+ *
+ * <p>Instead of maintaining them as an {@code Enum}, ServiceLoaders would provide a pluggable alternative.</p>
+ *
+ * @since 2.0
+ */
+public final class HashProvider {
+
+    private HashProvider() {
+        // utility class
+    }
+
+    /**
+     * Find a KDF implementation by searching the algorithms.
+     *
+     * @param algorithmName the algorithmName to match. This is case-sensitive.
+     * @return an instance of {@link HashProvider} if found, otherwise {@link Optional#empty()}.
+     * @throws NullPointerException if the given parameter algorithmName is {@code null}.
+     */
+    public static Optional<HashSpi> getByAlgorithmName(String algorithmName) {
+        requireNonNull(algorithmName, "algorithmName in HashProvider.getByAlgorithmName");
+        ServiceLoader<HashSpi> hashSpis = load();
+
+        return StreamSupport.stream(hashSpis.spliterator(), false)
+                .filter(hashSpi -> hashSpi.getImplementedAlgorithms().contains(algorithmName))
+                .findAny();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static ServiceLoader<HashSpi> load() {
+        return ServiceLoader.load(HashSpi.class);
+    }
+
+}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java
index 79d3251..2f0232c 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java
@@ -19,6 +19,13 @@
 package org.apache.shiro.crypto.hash;
 
 import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.Objects.requireNonNull;
 
 /**
  * A {@code HashRequest} is composed of data that will be used by a {@link HashService} to compute a hash (aka
@@ -49,19 +56,7 @@ public interface HashRequest {
      * @return a salt to be used by the {@link HashService} during hash computation, or {@code null} if no salt is
      *         provided as part of the request.
      */
-    ByteSource getSalt();
-
-    /**
-     * Returns the number of requested hash iterations to be performed when computing the final {@code Hash} result.
-     * A non-positive (0 or less) indicates that the {@code HashService}'s default iteration configuration should
-     * be used.  A positive value overrides the {@code HashService}'s configuration for a single request.
-     * <p/>
-     * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient
-     * to meet a desired level of security.
-     *
-     * @return the number of requested hash iterations to be performed when computing the final {@code Hash} result.
-     */
-    int getIterations();
+    Optional<ByteSource> getSalt();
 
     /**
      * Returns the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or
@@ -72,9 +67,26 @@ public interface HashRequest {
      * sufficient to meet a desired level of security.
      *
      * @return the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or
-     *         {@code null} if the default algorithm configuration of the {@code HashService} should be used.
+     * {@code null} if the default algorithm configuration of the {@code HashService} should be used.
+     */
+    Optional<String> getAlgorithmName();
+
+    /**
+     * Returns various parameters for the requested hash.
+     *
+     * <p>If the map is empty for a specific parameter, the implementation must select the default.</p>
+     *
+     * <p>Implementations should provide a nested {@code .Parameters} class with {@code public static final String}s
+     * for convenience.</p>
+     *
+     * <p>Example parameters the number of requested hash iterations (does not apply to bcrypt),
+     * memory and cpu constrains, etc.
+     * Please find their specific names in the implementation’s nested {@code .Parameters} class.</p>
+     *
+     * @return the parameters for the requested hash to be used when computing the final {@code Hash} result.
+     * @throws NullPointerException if any of the values is {@code null}.
      */
-    String getAlgorithmName();
+    Map<String, Object> getParameters();
 
     /**
      * A Builder class representing the Builder design pattern for constructing {@link HashRequest} instances.
@@ -85,15 +97,14 @@ public interface HashRequest {
     public static class Builder {
 
         private ByteSource source;
-        private ByteSource salt;
-        private int iterations;
+        private ByteSource salt = SimpleByteSource.empty();
+        private Map<String, Object> parameters = new ConcurrentHashMap<>();
         private String algorithmName;
 
         /**
          * Default no-arg constructor.
          */
         public Builder() {
-            this.iterations = 0;
         }
 
         /**
@@ -170,24 +181,14 @@ public interface HashRequest {
             return this;
         }
 
-        /**
-         * Sets the number of requested hash iterations to be performed when computing the final {@code Hash} result.
-         * Not calling this method or setting a non-positive value (0 or less) indicates that the {@code HashService}'s
-         * default iteration configuration should be used.  A positive value overrides the {@code HashService}'s
-         * configuration for a single request.
-         * <p/>
-         * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient
-         * to meet a desired level of security. You can always check the result
-         * {@code Hash} {@link Hash#getIterations() getIterations()} method to see what the actual
-         * number of iterations was, which may or may not match this request salt.
-         *
-         * @param iterations the number of requested hash iterations to be performed when computing the final
-         *                   {@code Hash} result.
-         * @return this {@code Builder} instance for method chaining.
-         * @see HashRequest#getIterations()
-         */
-        public Builder setIterations(int iterations) {
-            this.iterations = iterations;
+        public Builder addParameter(String parameterName, Object parameterValue) {
+            this.parameters.put(parameterName, requireNonNull(parameterValue));
+            return this;
+        }
+
+        public Builder withParameters(Map<String, Object> parameters) {
+            this.parameters.clear();
+            this.parameters.putAll(requireNonNull(parameters));
             return this;
         }
 
@@ -219,7 +220,7 @@ public interface HashRequest {
          * @return a {@link HashRequest} instance reflecting the specified configuration.
          */
         public HashRequest build() {
-            return new SimpleHashRequest(this.algorithmName, this.source, this.salt, this.iterations);
+            return new SimpleHashRequest(this.algorithmName, this.source, this.salt, this.parameters);
         }
     }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java
new file mode 100644
index 0000000..de4f2cf
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.hash;
+
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * Service Provider Interface for password hashing algorithms.
+ *
+ * <p>Apache Shiro will load algorithm implementations based on the method {@link #getImplementedAlgorithms()}.
+ * Loaded providers are expected to return a suitable hash implementation.</p>
+ *
+ * <p>Modern kdf-based hash implementations can extend the {@link AbstractCryptHash} class.</p>
+ *
+ * @since 2.0
+ */
+public interface HashSpi {
+
+    /**
+     * A list of algorithms recognized by this implementation.
+     *
+     * <p>Example values are {@code argon2id} and {@code argon2i} for the Argon2 service provider and
+     * {@code 2y} and {@code 2a} for the BCrypt service provider.</p>
+     *
+     * @return a set of recognized algorithms.
+     */
+    Set<String> getImplementedAlgorithms();
+
+    /**
+     * Creates a Hash instance from the given format string recognized by this provider.
+     *
+     * <p>There is no global format which this provider must accept. Each provider can define their own
+     * format, but they are usually based on the {@code crypt(3)} formats used in {@code /etc/shadow} files.</p>
+     *
+     * <p>Implementations should overwrite this javadoc to add examples of the accepted formats.</p>
+     *
+     * @param format the format string to be parsed by this implementation.
+     * @return a class extending Hash.
+     */
+    Hash fromString(String format);
+
+    /**
+     * A factory class for the hash of the type {@code <T>}.
+     *
+     * <p>Implementations are highly encouraged to use the given random parameter as
+     * source of random bytes (e.g. for seeds).</p>
+     *
+     * @param random a source of {@link Random}, usually {@code SecureRandom}.
+     * @return a factory class for creating instances of {@code <T>}.
+     */
+    HashFactory newHashFactory(Random random);
+
+    interface HashFactory {
+
+        /**
+         * Generates a hash from the given hash request.
+         *
+         * <p>If the hash requests’ optional parameters are not set, the {@link HashFactory} implementation
+         * should use default parameters where applicable.</p>
+         * <p>If the hash requests’ salt is missing or empty, the implementation should create a salt
+         * with a default size.</p>
+         * @param hashRequest the request to build a Hash from.
+         * @return a generated Hash according to the specs.
+         * @throws IllegalArgumentException if any of the parameters is outside of valid boundaries (algorithm-specific)
+         * or if the given algorithm is not applicable for this {@link HashFactory}.
+         */
+        Hash generate(HashRequest hashRequest);
+    }
+}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java
deleted file mode 100644
index dbfb9cb..0000000
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.crypto.hash;
-
-import org.apache.shiro.lang.codec.Base64;
-import org.apache.shiro.lang.codec.Hex;
-
-
-/**
- * Generates an MD2 Hash (RFC 1319) from a given input <tt>source</tt> with an optional <tt>salt</tt> and
- * hash iterations.
- * <p/>
- * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing
- * techniques and how the overloaded constructors function.
- *
- * @since 0.9
- */
-public class Md2Hash extends SimpleHash {
-
-    public static final String ALGORITHM_NAME = "MD2";
-
-    public Md2Hash() {
-        super(ALGORITHM_NAME);
-    }
-
-    public Md2Hash(Object source) {
-        super(ALGORITHM_NAME, source);
-    }
-
-    public Md2Hash(Object source, Object salt) {
-        super(ALGORITHM_NAME, source, salt);
-    }
-
-    public Md2Hash(Object source, Object salt, int hashIterations) {
-        super(ALGORITHM_NAME, source, salt, hashIterations);
-    }
-
-    public static Md2Hash fromHexString(String hex) {
-        Md2Hash hash = new Md2Hash();
-        hash.setBytes(Hex.decode(hex));
-        return hash;
-    }
-
-    public static Md2Hash fromBase64String(String base64) {
-        Md2Hash hash = new Md2Hash();
-        hash.setBytes(Base64.decode(base64));
-        return hash;
-    }
-}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java
deleted file mode 100644
index a83740a..0000000
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.crypto.hash;
-
-import org.apache.shiro.lang.codec.Base64;
-import org.apache.shiro.lang.codec.Hex;
-
-/**
- * Generates an MD5 Hash (RFC 1321) from a given input <tt>source</tt> with an optional <tt>salt</tt> and
- * hash iterations.
- * <p/>
- * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing
- * techniques and how the overloaded constructors function.
- *
- * @since 0.9
- */
-public class Md5Hash extends SimpleHash {
-
-    //TODO - complete JavaDoc
-
-    public static final String ALGORITHM_NAME = "MD5";
-
-    public Md5Hash() {
-        super(ALGORITHM_NAME);
-    }
-
-    public Md5Hash(Object source) {
-        super(ALGORITHM_NAME, source);
-    }
-
-    public Md5Hash(Object source, Object salt) {
-        super(ALGORITHM_NAME, source, salt);
-    }
-
-    public Md5Hash(Object source, Object salt, int hashIterations) {
-        super(ALGORITHM_NAME, source, salt, hashIterations);
-    }
-
-    public static Md5Hash fromHexString(String hex) {
-        Md5Hash hash = new Md5Hash();
-        hash.setBytes(Hex.decode(hex));
-        return hash;
-    }
-
-    public static Md5Hash fromBase64String(String base64) {
-        Md5Hash hash = new Md5Hash();
-        hash.setBytes(Base64.decode(base64));
-        return hash;
-    }
-}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java
deleted file mode 100644
index e844b70..0000000
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
- *
- *     http://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.
- */
-package org.apache.shiro.crypto.hash;
-
-import org.apache.shiro.lang.codec.Base64;
-import org.apache.shiro.lang.codec.Hex;
-
-
-/**
- * Generates an SHA-1 Hash (Secure Hash Standard, NIST FIPS 180-1) from a given input <tt>source</tt> with an
- * optional <tt>salt</tt> and hash iterations.
- * <p/>
- * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing
- * techniques and how the overloaded constructors function.
- *
- * @since 0.9
- */
-public class Sha1Hash extends SimpleHash {
-
-    //TODO - complete JavaDoc
-
-    public static final String ALGORITHM_NAME = "SHA-1";
-
-    public Sha1Hash() {
-        super(ALGORITHM_NAME);
-    }
-
-    public Sha1Hash(Object source) {
-        super(ALGORITHM_NAME, source);
-    }
-
-    public Sha1Hash(Object source, Object salt) {
-        super(ALGORITHM_NAME, source, salt);
-    }
-
-    public Sha1Hash(Object source, Object salt, int hashIterations) {
-        super(ALGORITHM_NAME, source, salt, hashIterations);
-    }
-
-    public static Sha1Hash fromHexString(String hex) {
-        Sha1Hash hash = new Sha1Hash();
-        hash.setBytes(Hex.decode(hex));
-        return hash;
-    }
-
-    public static Sha1Hash fromBase64String(String base64) {
-        Sha1Hash hash = new Sha1Hash();
-        hash.setBytes(Base64.decode(base64));
-        return hash;
-    }
-}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java
index 8c1fb6e..eb58a89 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java
@@ -18,17 +18,22 @@
  */
 package org.apache.shiro.crypto.hash;
 
+import org.apache.shiro.crypto.UnknownAlgorithmException;
 import org.apache.shiro.lang.codec.Base64;
 import org.apache.shiro.lang.codec.CodecException;
 import org.apache.shiro.lang.codec.Hex;
-import org.apache.shiro.crypto.UnknownAlgorithmException;
 import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
 import org.apache.shiro.lang.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 
+import static java.util.Objects.requireNonNull;
+
 /**
  * A {@code Hash} implementation that allows any {@link java.security.MessageDigest MessageDigest} algorithm name to
  * be used.  This class is a less type-safe variant than the other {@code AbstractHash} subclasses
@@ -43,6 +48,9 @@ import java.util.Arrays;
 public class SimpleHash extends AbstractHash {
 
     private static final int DEFAULT_ITERATIONS = 1;
+    private static final long serialVersionUID = -6689895264902387303L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(SimpleHash.class);
 
     /**
      * The {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash.
@@ -114,7 +122,7 @@ public class SimpleHash extends AbstractHash {
      */
     public SimpleHash(String algorithmName, Object source) throws CodecException, UnknownAlgorithmException {
         //noinspection NullableProblems
-        this(algorithmName, source, null, DEFAULT_ITERATIONS);
+        this(algorithmName, source, SimpleByteSource.empty(), DEFAULT_ITERATIONS);
     }
 
     /**
@@ -140,6 +148,28 @@ public class SimpleHash extends AbstractHash {
     }
 
     /**
+     * Creates an {@code algorithmName}-specific hash of the specified {@code source} using the given {@code salt}
+     * using a single hash iteration.
+     * <p/>
+     * It is a convenience constructor that merely executes <code>this( algorithmName, source, salt, 1);</code>.
+     * <p/>
+     * Please see the
+     * {@link #SimpleHash(String algorithmName, Object source, Object salt, int numIterations) SimpleHashHash(algorithmName, Object,Object,int)}
+     * constructor for the types of Objects that may be passed into this constructor, as well as how to support further
+     * types.
+     *
+     * @param algorithmName  the {@link java.security.MessageDigest MessageDigest} algorithm name to use when
+     *                       performing the hash.
+     * @param source         the source object to be hashed.
+     * @param hashIterations the number of times the {@code source} argument hashed for attack resiliency.
+     * @throws CodecException            if either constructor argument cannot be converted into a byte array.
+     * @throws UnknownAlgorithmException if the {@code algorithmName} is not available.
+     */
+    public SimpleHash(String algorithmName, Object source, int hashIterations) throws CodecException, UnknownAlgorithmException {
+        this(algorithmName, source, SimpleByteSource.empty(), hashIterations);
+    }
+
+    /**
      * Creates an {@code algorithmName}-specific hash of the specified {@code source} using the given
      * {@code salt} a total of {@code hashIterations} times.
      * <p/>
@@ -169,11 +199,8 @@ public class SimpleHash extends AbstractHash {
         }
         this.algorithmName = algorithmName;
         this.iterations = Math.max(DEFAULT_ITERATIONS, hashIterations);
-        ByteSource saltBytes = null;
-        if (salt != null) {
-            saltBytes = convertSaltToBytes(salt);
-            this.salt = saltBytes;
-        }
+        ByteSource saltBytes = convertSaltToBytes(salt);
+        this.salt = saltBytes;
         ByteSource sourceBytes = convertSourceToBytes(source);
         hash(sourceBytes, saltBytes, hashIterations);
     }
@@ -209,23 +236,20 @@ public class SimpleHash extends AbstractHash {
     /**
      * Converts a given object into a {@code ByteSource} instance.  Assumes the object can be converted to bytes.
      *
-     * @param o the Object to convert into a {@code ByteSource} instance.
+     * @param object the Object to convert into a {@code ByteSource} instance.
      * @return the {@code ByteSource} representation of the specified object's bytes.
      * @since 1.2
      */
-    protected ByteSource toByteSource(Object o) {
-        if (o == null) {
-            return null;
+    protected ByteSource toByteSource(Object object) {
+        if (object instanceof ByteSource) {
+            return (ByteSource) object;
         }
-        if (o instanceof ByteSource) {
-            return (ByteSource) o;
-        }
-        byte[] bytes = toBytes(o);
+        byte[] bytes = toBytes(object);
         return ByteSource.Util.bytes(bytes);
     }
 
     private void hash(ByteSource source, ByteSource salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
-        byte[] saltBytes = salt != null ? salt.getBytes() : null;
+        byte[] saltBytes = requireNonNull(salt).getBytes();
         byte[] hashedBytes = hash(source.getBytes(), saltBytes, hashIterations);
         setBytes(hashedBytes);
     }
@@ -235,18 +259,34 @@ public class SimpleHash extends AbstractHash {
      *
      * @return the {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash.
      */
+    @Override
     public String getAlgorithmName() {
         return this.algorithmName;
     }
 
+    @Override
     public ByteSource getSalt() {
         return this.salt;
     }
 
+    @Override
     public int getIterations() {
         return this.iterations;
     }
 
+    @Override
+    public boolean matchesPassword(ByteSource plaintextBytes) {
+        try {
+            SimpleHash otherHash = new SimpleHash(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations());
+            return this.equals(otherHash);
+        } catch (IllegalArgumentException illegalArgumentException) {
+            // cannot recreate hash. Do not log password.
+            LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException);
+            return false;
+        }
+    }
+
+    @Override
     public byte[] getBytes() {
         return this.bytes;
     }
@@ -259,6 +299,7 @@ public class SimpleHash extends AbstractHash {
      *
      * @param alreadyHashedBytes the raw already-hashed bytes to store in this instance.
      */
+    @Override
     public void setBytes(byte[] alreadyHashedBytes) {
         this.bytes = alreadyHashedBytes;
         this.hexEncoded = null;
@@ -298,6 +339,7 @@ public class SimpleHash extends AbstractHash {
      * @return the MessageDigest object for the specified {@code algorithm}.
      * @throws UnknownAlgorithmException if the specified algorithm name is not available.
      */
+    @Override
     protected MessageDigest getDigest(String algorithmName) throws UnknownAlgorithmException {
         try {
             return MessageDigest.getInstance(algorithmName);
@@ -314,6 +356,7 @@ public class SimpleHash extends AbstractHash {
      * @return the hashed bytes.
      * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available.
      */
+    @Override
     protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException {
         return hash(bytes, null, DEFAULT_ITERATIONS);
     }
@@ -326,6 +369,7 @@ public class SimpleHash extends AbstractHash {
      * @return the hashed bytes
      * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available.
      */
+    @Override
     protected byte[] hash(byte[] bytes, byte[] salt) throws UnknownAlgorithmException {
         return hash(bytes, salt, DEFAULT_ITERATIONS);
     }
@@ -339,9 +383,10 @@ public class SimpleHash extends AbstractHash {
      * @return the hashed bytes.
      * @throws UnknownAlgorithmException if the {@link #getAlgorithmName() algorithmName} is not available.
      */
+    @Override
     protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws UnknownAlgorithmException {
         MessageDigest digest = getDigest(getAlgorithmName());
-        if (salt != null) {
+        if (salt.length != 0) {
             digest.reset();
             digest.update(salt);
         }
@@ -355,6 +400,7 @@ public class SimpleHash extends AbstractHash {
         return hashed;
     }
 
+    @Override
     public boolean isEmpty() {
         return this.bytes == null || this.bytes.length == 0;
     }
@@ -368,6 +414,7 @@ public class SimpleHash extends AbstractHash {
      *
      * @return a hex-encoded string of the underlying {@link #getBytes byte array}.
      */
+    @Override
     public String toHex() {
         if (this.hexEncoded == null) {
             this.hexEncoded = Hex.encodeToString(getBytes());
@@ -384,6 +431,7 @@ public class SimpleHash extends AbstractHash {
      *
      * @return a Base64-encoded string of the underlying {@link #getBytes byte array}.
      */
+    @Override
     public String toBase64() {
         if (this.base64Encoded == null) {
             //cache result in case this method is called multiple times.
@@ -397,6 +445,7 @@ public class SimpleHash extends AbstractHash {
      *
      * @return the {@link #toHex() toHex()} value.
      */
+    @Override
     public String toString() {
         return toHex();
     }
@@ -409,6 +458,7 @@ public class SimpleHash extends AbstractHash {
      * @return {@code true} if the specified object is a Hash and its {@link #getBytes byte array} is identical to
      *         this Hash's byte array, {@code false} otherwise.
      */
+    @Override
     public boolean equals(Object o) {
         if (o instanceof Hash) {
             Hash other = (Hash) o;
@@ -422,6 +472,7 @@ public class SimpleHash extends AbstractHash {
      *
      * @return toHex().hashCode()
      */
+    @Override
     public int hashCode() {
         if (this.bytes == null || this.bytes.length == 0) {
             return 0;
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java
new file mode 100644
index 0000000..5b4a44d
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.hash;
+
+import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+import static java.util.Collections.unmodifiableSet;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Creates a hash provider for salt (+pepper) and Hash-based KDFs, i.e. where the algorithm name
+ * is a SHA algorithm or similar.
+ * @since 2.0
+ */
+public class SimpleHashProvider implements HashSpi {
+
+    private static final Set<String> IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[]{
+            Sha256Hash.ALGORITHM_NAME,
+            Sha384Hash.ALGORITHM_NAME,
+            Sha512Hash.ALGORITHM_NAME
+    })
+            .collect(toSet());
+
+    @Override
+    public Set<String> getImplementedAlgorithms() {
+        return unmodifiableSet(IMPLEMENTED_ALGORITHMS);
+    }
+
+    @Override
+    public SimpleHash fromString(String format) {
+        Hash hash = new Shiro1CryptFormat().parse(format);
+
+        if (!(hash instanceof SimpleHash)) {
+            throw new IllegalArgumentException("formatted string was not a simple hash: " + format);
+        }
+
+        return (SimpleHash) hash;
+    }
+
+    @Override
+    public HashFactory newHashFactory(Random random) {
+        return new SimpleHashFactory(random);
+    }
+
+    static class SimpleHashFactory implements HashSpi.HashFactory {
+
+        private final Random random;
+
+        public SimpleHashFactory(Random random) {
+            this.random = random;
+        }
+
+        @Override
+        public SimpleHash generate(HashRequest hashRequest) {
+            String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM);
+            ByteSource source = hashRequest.getSource();
+            final int iterations = getIterations(hashRequest);
+
+            final ByteSource publicSalt = getPublicSalt(hashRequest);
+            final /*nullable*/ ByteSource secretSalt = getSecretSalt(hashRequest);
+            final ByteSource salt = combine(secretSalt, publicSalt);
+
+            return createSimpleHash(algorithmName, source, iterations, publicSalt, salt);
+        }
+
+        /**
+         * Returns the public salt that should be used to compute a hash based on the specified request.
+         * <p/>
+         * This implementation functions as follows:
+         * <ol>
+         *   <li>If the request salt is not null and non-empty, this will be used, return it.</li>
+         *   <li>If the request salt is null or empty:
+         *     <ol><li>create a new 16-byte salt.</li></ol>
+         *   </li>
+         * </ol>
+         *
+         * @param request request the request to process
+         * @return the public salt that should be used to compute a hash based on the specified request or
+         * {@code null} if no public salt should be used.
+         */
+        protected ByteSource getPublicSalt(HashRequest request) {
+            Optional<ByteSource> publicSalt = request.getSalt();
+
+            if (publicSalt.isPresent() && !publicSalt.orElseThrow(NoSuchElementException::new).isEmpty()) {
+                //a public salt was explicitly requested to be used - go ahead and use it:
+                return publicSalt.orElseThrow(NoSuchElementException::new);
+            }
+
+            // generate salt if absent from the request.
+            byte[] ps = new byte[16];
+            random.nextBytes(ps);
+
+            return new SimpleByteSource(ps);
+        }
+
+        private ByteSource getSecretSalt(HashRequest request) {
+            Optional<Object> secretSalt = Optional.ofNullable(request.getParameters().get(Parameters.PARAMETER_SECRET_SALT));
+
+            return secretSalt
+                    .map(salt -> (String) salt)
+                    .map(salt -> Base64.getDecoder().decode(salt))
+                    .map(SimpleByteSource::new)
+                    .orElse(null);
+        }
+
+        private SimpleHash createSimpleHash(String algorithmName, ByteSource source, int iterations, ByteSource publicSalt, ByteSource salt) {
+            Hash computed = new SimpleHash(algorithmName, source, salt, iterations);
+
+            SimpleHash result = new SimpleHash(algorithmName);
+            result.setBytes(computed.getBytes());
+            result.setIterations(iterations);
+            //Only expose the public salt - not the real/combined salt that might have been used:
+            result.setSalt(publicSalt);
+
+            return result;
+        }
+
+        protected int getIterations(HashRequest request) {
+            Object parameterIterations = request.getParameters().getOrDefault(Parameters.PARAMETER_ITERATIONS, 0);
+
+            if (!(parameterIterations instanceof Integer)) {
+                return Parameters.DEFAULT_ITERATIONS;
+            }
+
+            final int iterations = Math.max(0, (Integer) parameterIterations);
+
+            if (iterations < 1) {
+                return Parameters.DEFAULT_ITERATIONS;
+            }
+
+            return iterations;
+        }
+
+        /**
+         * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the
+         * total salt during hash computation.  {@code privateSaltBytes} will be {@code null} }if no private salt has been
+         * configured.
+         *
+         * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes
+         * @param publicSalt  the extra bytes to use in addition to the given private salt.
+         * @return a combination of the specified private salt bytes and extra bytes that will be used as the total
+         * salt during hash computation.
+         */
+        protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) {
+
+            // optional 'pepper'
+            byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null;
+            int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0;
+
+            // salt must always be present.
+            byte[] publicSaltBytes = publicSalt.getBytes();
+            int extraBytesLength = publicSaltBytes.length;
+
+            int length = privateSaltLength + extraBytesLength;
+
+            if (length <= 0) {
+                return SimpleByteSource.empty();
+            }
+
+            byte[] combined = new byte[length];
+
+            int i = 0;
+            for (int j = 0; j < privateSaltLength; j++) {
+                combined[i++] = privateSaltBytes[j];
+            }
+            for (int j = 0; j < extraBytesLength; j++) {
+                combined[i++] = publicSaltBytes[j];
+            }
+
+            return ByteSource.Util.bytes(combined);
+        }
+    }
+
+    static final class Parameters {
+        public static final String PARAMETER_ITERATIONS = "SimpleHash.iterations";
+
+        /**
+         * A secret part added to the salt. Sometimes also referred to as {@literal "Pepper"}.
+         *
+         * <p>For more information, see <a href="https://en.wikipedia.org/wiki/Pepper_(cryptography)">Pepper (cryptography) on Wikipedia</a>.</p>
+         */
+        public static final String PARAMETER_SECRET_SALT = "SimpleHash.secretSalt";
+
+        public static final String DEFAULT_ALGORITHM = "SHA-512";
+
+        public static final int DEFAULT_ITERATIONS = 50_000;
+
+
+        private Parameters() {
+            // util class
+        }
+    }
+}
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java
index 5423256..ffd2989 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java
@@ -20,6 +20,13 @@ package org.apache.shiro.crypto.hash;
 
 import org.apache.shiro.lang.util.ByteSource;
 
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.Collections.unmodifiableMap;
+import static java.util.Objects.requireNonNull;
+
 /**
  * Simple implementation of {@link HashRequest} that can be used when interacting with a {@link HashService}.
  *
@@ -29,46 +36,46 @@ public class SimpleHashRequest implements HashRequest {
 
     private final ByteSource source; //cannot be null - this is the source to hash.
     private final ByteSource salt; //null = no salt specified
-    private final int iterations; //0 = not specified by the requestor; let the HashService decide.
     private final String algorithmName; //null = let the HashService decide.
+    private final Map<String, Object> parameters = new ConcurrentHashMap<>();
 
     /**
      * Creates a new SimpleHashRequest instance.
      *
      * @param algorithmName the name of the hash algorithm to use.  This is often null as the
-     * {@link HashService} implementation is usually configured with an appropriate algorithm name, but this
-     * can be non-null if the hash service's algorithm should be overridden with a specific one for the duration
-     * of the request.
-     *
-     * @param source the source to be hashed
-     * @param salt any public salt which should be used when computing the hash
-     * @param iterations the number of hash iterations to execute.  Zero (0) indicates no iterations were specified
-     * for the request, at which point the number of iterations is decided by the {@code HashService}
-     * @throws NullPointerException if {@code source} is null or empty.
+     *                      {@link HashService} implementation is usually configured with an appropriate algorithm name, but this
+     *                      can be non-null if the hash service's algorithm should be overridden with a specific one for the duration
+     *                      of the request.
+     * @param source        the source to be hashed
+     * @param salt          any public salt which should be used when computing the hash
+     * @param parameters    e.g. the number of hash iterations to execute or other parameters.
+     * @throws NullPointerException if {@code source} is null or empty or {@code parameters} is {@code null}.
      */
-    public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, int iterations) {
-        if (source == null) {
-            throw new NullPointerException("source argument cannot be null");
-        }
-        this.source = source;
+    public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, Map<String, Object> parameters) {
+        this.source = requireNonNull(source);
         this.salt = salt;
         this.algorithmName = algorithmName;
-        this.iterations = Math.max(0, iterations);
+        this.parameters.putAll(requireNonNull(parameters));
     }
 
+    @Override
     public ByteSource getSource() {
         return this.source;
     }
 
-    public ByteSource getSalt() {
-        return this.salt;
+    @Override
+    public Optional<ByteSource> getSalt() {
+        return Optional.ofNullable(this.salt);
     }
 
-    public int getIterations() {
-        return iterations;
+
+    @Override
+    public Optional<String> getAlgorithmName() {
+        return Optional.ofNullable(algorithmName);
     }
 
-    public String getAlgorithmName() {
-        return algorithmName;
+    @Override
+    public Map<String, Object> getParameters() {
+        return unmodifiableMap(this.parameters);
     }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java
index 78742c0..35b3394 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java
@@ -20,22 +20,28 @@ package org.apache.shiro.crypto.hash.format;
 
 import org.apache.shiro.crypto.hash.Hash;
 
+import static java.util.Objects.requireNonNull;
+
 /**
  * {@code HashFormat} that outputs <em>only</em> the hash's digest bytes in Base64 format.  It does not print out
  * anything else (salt, iterations, etc).  This implementation is mostly provided as a convenience for
  * command-line hashing.
  *
  * @since 1.2
+ * @deprecated will throw exceptions in 2.1.0, to be removed in 2.2.0
  */
+@Deprecated
 public class Base64Format implements HashFormat {
 
     /**
-     * Returns {@code hash != null ? hash.toBase64() : null}.
+     * Returns {@code hash.toBase64()}.
      *
      * @param hash the hash instance to format into a String.
-     * @return {@code hash != null ? hash.toBase64() : null}.
+     * @return {@code hash.toBase64()}.
+     * @throws NullPointerException if hash is {@code null}.
      */
-    public String format(Hash hash) {
-        return hash != null ? hash.toBase64() : null;
+    @Override
+    public String format(final Hash hash) {
+        return requireNonNull(hash).toBase64();
     }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java
index 34553d9..ae09b13 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java
@@ -111,13 +111,14 @@ public class DefaultHashFormatFactory implements HashFormatFactory {
         this.searchPackages = searchPackages;
     }
 
+    @Override
     public HashFormat getInstance(String in) {
         if (in == null) {
             return null;
         }
 
         HashFormat hashFormat = null;
-        Class clazz = null;
+        Class<?> clazz = null;
 
         //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance
         //optimization.  If the input arg is an MCF-formatted string, there will be many unnecessary ClassLoader
@@ -128,7 +129,7 @@ public class DefaultHashFormatFactory implements HashFormatFactory {
             String test = in.substring(ModularCryptFormat.TOKEN_DELIMITER.length());
             String[] tokens = test.split("\\" + ModularCryptFormat.TOKEN_DELIMITER);
             //the MCF ID is always the first token in the delimited string:
-            String possibleMcfId = (tokens != null && tokens.length > 0) ? tokens[0] : null;
+            String possibleMcfId = tokens.length > 0 ? tokens[0] : null;
             if (possibleMcfId != null) {
                 //found a possible MCF ID - test it using our heuristics to see if we can find a corresponding class:
                 clazz = getHashFormatClass(possibleMcfId);
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java
index c65ae78..29d8535 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java
@@ -24,13 +24,11 @@ import org.apache.shiro.crypto.hash.Hash;
  * A {@code HashFormat} is able to format a {@link Hash} instance into a well-defined formatted String.
  * <p/>
  * Note that not all HashFormat algorithms are reversible.  That is, they can't be parsed and reconstituted to the
- * original Hash instance.  The traditional <a href="http://en.wikipedia.org/wiki/Crypt_(Unix)">
- * Unix crypt(3)</a> is one such format.
+ * original Hash instance.
  * <p/>
  * The formats that <em>are</em> reversible however will be represented as {@link ParsableHashFormat} instances.
  *
  * @see ParsableHashFormat
- *
  * @since 1.2
  */
 public interface HashFormat {
@@ -40,6 +38,7 @@ public interface HashFormat {
      *
      * @param hash the hash instance to format into a String.
      * @return a formatted string representing the specified Hash instance.
+     * @throws NullPointerException if given parameter hash is {@code null}.
      */
     String format(Hash hash);
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java
index 5730ac9..2dfb802 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java
@@ -26,16 +26,20 @@ import org.apache.shiro.crypto.hash.Hash;
  * command-line hashing.
  *
  * @since 1.2
+ * @deprecated will throw exceptions in 2.1.0, to be removed in 2.2.0
  */
+@Deprecated
 public class HexFormat implements HashFormat {
 
     /**
-     * Returns {@code hash != null ? hash.toHex() : null}.
+     * Returns {@code hash.toHex()}.
      *
      * @param hash the hash instance to format into a String.
-     * @return {@code hash != null ? hash.toHex() : null}.
+     * @return {@code hash.toHex()}.
+     * @throws NullPointerException if given parameter hash is {@code null}.
      */
-    public String format(Hash hash) {
-        return hash != null ? hash.toHex() : null;
+    @Override
+    public String format(final Hash hash) {
+        return hash.toHex();
     }
 }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java
index 3813123..9ed5246 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java
@@ -40,11 +40,16 @@ public enum ProvidedHashFormat {
     /**
      * Value representing the {@link Shiro1CryptFormat} implementation.
      */
-    SHIRO1(Shiro1CryptFormat.class);
+    SHIRO1(Shiro1CryptFormat.class),
+
+    /**
+     * Value representing the {@link Shiro2CryptFormat} implementation.
+     */
+    SHIRO2(Shiro2CryptFormat.class);
 
     private final Class<? extends HashFormat> clazz;
 
-    private ProvidedHashFormat(Class<? extends HashFormat> clazz) {
+    ProvidedHashFormat(final Class<? extends HashFormat> clazz) {
         this.clazz = clazz;
     }
 
@@ -52,7 +57,7 @@ public enum ProvidedHashFormat {
         return this.clazz;
     }
 
-    public static ProvidedHashFormat byId(String id) {
+    public static ProvidedHashFormat byId(final String id) {
         if (id == null) {
             return null;
         }
@@ -60,7 +65,7 @@ public enum ProvidedHashFormat {
             // Use English Locale, some Locales handle uppercase/lower differently. i.e. Turkish and upper case 'i'
             // is not 'I'. And 'SHIRO1' would be 'SHİRO1'
             return valueOf(id.toUpperCase(Locale.ENGLISH));
-        } catch (IllegalArgumentException ignored) {
+        } catch (final IllegalArgumentException ignored) {
             return null;
         }
     }
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java
index 24966ea..1428f3a 100644
--- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java
@@ -18,9 +18,10 @@
  */
 package org.apache.shiro.crypto.hash.format;
 
-import org.apache.shiro.lang.codec.Base64;
 import org.apache.shiro.crypto.hash.Hash;
 import org.apache.shiro.crypto.hash.SimpleHash;
+import org.apache.shiro.crypto.hash.SimpleHashProvider;
+import org.apache.shiro.lang.codec.Base64;
 import org.apache.shiro.lang.util.ByteSource;
 import org.apache.shiro.lang.util.StringUtils;
 
@@ -93,11 +94,13 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat
     public Shiro1CryptFormat() {
     }
 
+    @Override
     public String getId() {
         return ID;
     }
 
-    public String format(Hash hash) {
+    @Override
+    public String format(final Hash hash) {
         if (hash == null) {
             return null;
         }
@@ -117,7 +120,8 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat
         return sb.toString();
     }
 
-    public Hash parse(String formatted) {
+    @Override
+    public Hash parse(final String formatted) {
         if (formatted == null) {
             return null;
         }
@@ -130,13 +134,17 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat
         String suffix = formatted.substring(MCF_PREFIX.length());
         String[] parts = suffix.split("\\$");
 
+        final String algorithmName = parts[0];
+        if (!new SimpleHashProvider().getImplementedAlgorithms().contains(algorithmName)) {
+            throw new UnsupportedOperationException("Algorithm " + algorithmName + " is not supported in shiro1 format.");
+        }
+
         //last part is always the digest/checksum, Base64-encoded:
-        int i = parts.length-1;
+        int i = parts.length - 1;
         String digestBase64 = parts[i--];
         //second-to-last part is always the salt, Base64-encoded:
         String saltBase64 = parts[i--];
         String iterationsString = parts[i--];
-        String algorithmName = parts[i];
 
         byte[] digest = Base64.decode(digestBase64);
         ByteSource salt = null;
diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java
new file mode 100644
index 0000000..7cc3ff1
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+package org.apache.shiro.crypto.hash.format;
+
+import org.apache.shiro.crypto.hash.AbstractCryptHash;
+import org.apache.shiro.crypto.hash.Hash;
+import org.apache.shiro.crypto.hash.HashProvider;
+import org.apache.shiro.crypto.hash.HashSpi;
+import org.apache.shiro.crypto.hash.SimpleHash;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The {@code Shiro1CryptFormat} is a fully reversible
+ * <a href="http://packages.python.org/passlib/modular_crypt_format.html">Modular Crypt Format</a> (MCF). It is based
+ * on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems.
+ * <h2>Format</h2>
+ * <p>Hash instances formatted with this implementation will result in a String with the following dollar-sign ($)
+ * delimited format:</p>
+ * <pre>
+ * <b>$</b>mcfFormatId<b>$</b>algorithmName<b>$</b>algorithm-specific-data.
+ * </pre>
+ * <p>Each token is defined as follows:</p>
+ * <table>
+ *     <tr>
+ *         <th>Position</th>
+ *         <th>Token</th>
+ *         <th>Description</th>
+ *         <th>Required?</th>
+ *     </tr>
+ *     <tr>
+ *         <td>1</td>
+ *         <td>{@code mcfFormatId}</td>
+ *         <td>The Modular Crypt Format identifier for this implementation, equal to <b>{@code shiro2}</b>.
+ *             ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix
+ *             {@code $shiro2$} ).</td>
+ *         <td>true</td>
+ *     </tr>
+ *     <tr>
+ *         <td>2</td>
+ *         <td>{@code algorithmName}</td>
+ *         <td>The name of the hash algorithm used to perform the hash. Either a hash class exists, or
+ *         otherwise a {@link UnsupportedOperationException} will be thrown.
+ *         <td>true</td>
+ *     </tr>
+ *     <tr>
+ *         <td>3</td>
+ *         <td>{@code algorithm-specific-data}</td>
+ *         <td>In contrast to the previous {@code shiro1} format, the shiro2 format does not make any assumptions
+ *         about how an algorithm stores its data. Therefore, everything beyond the first token is handled over
+ *         to the Hash implementation.</td>
+ *     </tr>
+ * </table>
+ *
+ * @see ModularCryptFormat
+ * @see ParsableHashFormat
+ * @since 2.0
+ */
+public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat {
+
+    /**
+     * Identifier for the shiro2 crypt format.
+     */
+    public static final String ID = "shiro2";
+    /**
+     * Enclosed identifier of the shiro2 crypt format.
+     */
+    public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER;
+
+    public Shiro2CryptFormat() {
+    }
+
+    @Override
+    public String getId() {
+        return ID;
+    }
+
+    /**
+     * Converts a Hash-extending class to a string understood by the hash class. Usually this string will follow
+     * posix standards for passwords stored in {@code /etc/passwd}.
+     *
+     * <p>This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.</p>
+     *
+     * @param hash the hash instance to format into a String.
+     * @return a string representing the hash.
+     */
+    @Override
+    public String format(final Hash hash) {
+        requireNonNull(hash, "hash in Shiro2CryptFormat.format(Hash hash)");
+
+        // backwards compatibility until Shiro 2.1.0.
+        if (hash instanceof SimpleHash) {
+            return new Shiro1CryptFormat().format(hash);
+        }
+
+        if (!(hash instanceof AbstractCryptHash)) {
+            throw new UnsupportedOperationException("Shiro2CryptFormat can only format classes extending AbstractCryptHash.");
+        }
+
+        AbstractCryptHash cryptHash = (AbstractCryptHash) hash;
+        return TOKEN_DELIMITER + ID + cryptHash.formatToCryptString();
+    }
+
+    @Override
+    public Hash parse(final String formatted) {
+        requireNonNull(formatted, "formatted in Shiro2CryptFormat.parse(String formatted)");
+
+        // backwards compatibility until Shiro 2.1.0.
+        if (formatted.startsWith(Shiro1CryptFormat.MCF_PREFIX)) {
+            return new Shiro1CryptFormat().parse(formatted);
+        }
+
+        if (!formatted.startsWith(MCF_PREFIX)) {
+            final String msg = "The argument is not a valid '" + ID + "' formatted hash.";
+            throw new IllegalArgumentException(msg);
+        }
+
+        final String suffix = formatted.substring(MCF_PREFIX.length());
+        final String[] parts = suffix.split("\\$");
+        final String algorithmName = parts[0];
+
+        HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName)
+                .orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented."));
+        return kdfHash.fromString("$" + suffix);
+    }
+
+}
diff --git a/crypto/hash/src/main/resources/META-INF/NOTICE b/crypto/hash/src/main/resources/META-INF/NOTICE
index 9d26a95..5976d79 100644
--- a/crypto/hash/src/main/resources/META-INF/NOTICE
+++ b/crypto/hash/src/main/resources/META-INF/NOTICE
@@ -7,7 +7,7 @@ The Apache Software Foundation (http://www.apache.org/).
 The implementation for org.apache.shiro.util.SoftHashMap is based 
 on initial ideas from Dr. Heinz Kabutz's publicly posted version 
 available at http://www.javaspecialists.eu/archive/Issue015.html,
-with continued modifications.  
+with continued modifications.
 
 Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
 code for this  product was copied for simplicity and to reduce
diff --git a/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
new file mode 100644
index 0000000..dc9d0d2
--- /dev/null
+++ b/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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
+#
+#     http://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.
+#
+
+org.apache.shiro.crypto.hash.SimpleHashProvider
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy
index d021be2..389ba3a 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy
@@ -18,12 +18,10 @@
  */
 package org.apache.shiro.crypto.hash
 
-import org.apache.shiro.crypto.RandomNumberGenerator
-import org.apache.shiro.crypto.SecureRandomNumberGenerator
+
 import org.apache.shiro.lang.util.ByteSource
 import org.junit.Test
 
-import static org.easymock.EasyMock.*
 import static org.junit.Assert.*
 
 /**
@@ -35,54 +33,27 @@ class DefaultHashServiceTest {
 
     @Test
     void testNullRequest() {
-        assertNull createService().computeHash(null)
+        assertNull createSha256Service().computeHash(null)
     }
 
     @Test
     void testDifferentAlgorithmName() {
-        def service = new DefaultHashService(hashAlgorithmName: 'MD5')
-        def hash = hash(service, "test")
-        assertEquals 'MD5', hash.algorithmName
-    }
+        // given
+        def newAlgorithm = 'SHA-512'
+        def service = new DefaultHashService(defaultAlgorithmName: newAlgorithm)
 
-    @Test
-    void testDifferentIterations() {
-        def service = new DefaultHashService(hashIterations: 2)
+        // when
         def hash = hash(service, "test")
-        assertEquals 2, hash.iterations
-    }
-
-    @Test
-    void testDifferentRandomNumberGenerator() {
 
-        def ByteSource randomBytes = new SecureRandomNumberGenerator().nextBytes()
-        def rng = createMock(RandomNumberGenerator)
-        expect(rng.nextBytes()).andReturn randomBytes
-
-        replay rng
-
-        def service = new DefaultHashService(randomNumberGenerator: rng, generatePublicSalt: true)
-        hash(service, "test")
-
-        verify rng
-    }
-
-    /**
-     * If 'generatePublicSalt' is true, 2 hashes of the same input source should be different.
-     */
-    @Test
-    void testWithRandomlyGeneratedSalt() {
-        def service = new DefaultHashService(generatePublicSalt: true)
-        def first = hash(service, "password")
-        def second = hash(service, "password")
-        assertFalse first == second
+        // then
+        assertEquals newAlgorithm, hash.algorithmName
     }
 
     @Test
     void testRequestWithEmptySource() {
         def source = ByteSource.Util.bytes((byte[])null)
         def request = new HashRequest.Builder().setSource(source).build()
-        def service = createService()
+        def service = createSha256Service()
         assertNull service.computeHash(request)
     }
 
@@ -92,7 +63,7 @@ class DefaultHashServiceTest {
      */
     @Test
     void testOnlyRandomSaltHash() {
-        HashService service = createService();
+        HashService service = createSha256Service();
         Hash first = hash(service, "password");
         Hash second = hash(service, "password2", first.salt);
         assertFalse first == second
@@ -104,7 +75,7 @@ class DefaultHashServiceTest {
      */
     @Test
     void testBothSaltsRandomness() {
-        HashService service = createServiceWithPrivateSalt();
+        HashService service = createSha256Service();
         Hash first = hash(service, "password");
         Hash second = hash(service, "password");
         assertFalse first == second
@@ -117,7 +88,7 @@ class DefaultHashServiceTest {
      */
     @Test
     void testBothSaltsReturn() {
-        HashService service = createServiceWithPrivateSalt();
+        HashService service = createSha256Service();
         Hash first = hash(service, "password");
         Hash second = hash(service, "password", first.salt);
         assertEquals first, second
@@ -129,24 +100,12 @@ class DefaultHashServiceTest {
      */
     @Test
     void testBothSaltsHash() {
-        HashService service = createServiceWithPrivateSalt();
+        HashService service = createSha256Service();
         Hash first = hash(service, "password");
         Hash second = hash(service, "password2", first.salt);
         assertFalse first == second
     }
 
-    /**
-     * Hash result is different if the base salt is added.
-     */
-    @Test
-    public void testPrivateSaltChangesResult() {
-        HashService saltedService = createServiceWithPrivateSalt();
-        HashService service = createService();
-        Hash first = hashPredictable(saltedService, "password");
-        Hash second = hashPredictable(service, "password");
-        assertFalse first == second
-    }
-
     protected Hash hash(HashService hashService, def source) {
         return hashService.computeHash(new HashRequest.Builder().setSource(source).build());
     }
@@ -155,19 +114,8 @@ class DefaultHashServiceTest {
         return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build());
     }
 
-    private Hash hashPredictable(HashService hashService, def source) {
-        byte[] salt = new byte[20];
-        Arrays.fill(salt, (byte) 2);
-        return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build());
-    }
-
-    private DefaultHashService createService() {
-        return new DefaultHashService();
+    private static DefaultHashService createSha256Service() {
+        return new DefaultHashService(defaultAlgorithmName: 'SHA-256');
     }
 
-    private DefaultHashService createServiceWithPrivateSalt() {
-        DefaultHashService defaultHashService = new DefaultHashService();
-        defaultHashService.setPrivateSalt(new SecureRandomNumberGenerator().nextBytes());
-        return defaultHashService;
-    }
 }
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy
index 527098c..323de38 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy
@@ -20,9 +20,10 @@ package org.apache.shiro.crypto.hash
 
 import org.apache.shiro.crypto.SecureRandomNumberGenerator
 import org.apache.shiro.lang.util.ByteSource
-import org.junit.Test
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.*
 
-import static org.junit.Assert.*
 
 /**
  * Unit tests for the {@link HashRequest.Builder} implementation
@@ -33,16 +34,7 @@ class HashRequestBuilderTest {
 
     @Test
     void testNullSource() {
-        try {
-            new HashRequest.Builder().build()
-            fail "NullPointerException should be thrown"
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @Test
-    void testDefault() {
-        assertEquals 0, new HashRequest.Builder().setSource("test").build().iterations
+        assertThrows NullPointerException, { new HashRequest.Builder().build() }
     }
 
     @Test
@@ -50,15 +42,16 @@ class HashRequestBuilderTest {
         ByteSource source = ByteSource.Util.bytes("test")
         ByteSource salt = new SecureRandomNumberGenerator().nextBytes()
         def request = new HashRequest.Builder()
-            .setSource(source)
-            .setSalt(salt)
-            .setIterations(2)
-            .setAlgorithmName('MD5').build()
+                .setSource(source)
+                .setSalt(salt)
+                .addParameter(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS, 2)
+                .setAlgorithmName('MD5')
+                .build()
 
         assertNotNull request
         assertEquals source, request.source
-        assertEquals salt, request.salt
-        assertEquals 2, request.iterations
-        assertEquals 'MD5', request.algorithmName
+        assertEquals salt, request.salt.orElse(null)
+        assertEquals 2, request.getParameters().get(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS)
+        assertEquals 'MD5', request.algorithmName.orElse(null)
     }
 }
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy
index 75cb266..737eedc 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy
@@ -19,9 +19,11 @@
 package org.apache.shiro.crypto.hash.format
 
 import org.apache.shiro.crypto.hash.Hash
-import org.apache.shiro.crypto.hash.Sha1Hash
+import org.apache.shiro.crypto.hash.Sha512Hash
 import org.junit.Test
-import static org.junit.Assert.*
+
+import static org.junit.Assert.assertEquals
+import static org.junit.Assert.assertThrows
 
 /**
  * Unit tests for the {@link Base64Format} implementation.
@@ -32,7 +34,7 @@ class Base64FormatTest {
 
     @Test
     void testFormat() {
-        Hash hash = new Sha1Hash("hello");
+        Hash hash = new Sha512Hash("hello");
         Base64Format format = new Base64Format()
         String base64 = format.format(hash)
         assertEquals base64, hash.toBase64()
@@ -41,7 +43,7 @@ class Base64FormatTest {
     @Test
     void testFormatWithNullArgument() {
         Base64Format format = new Base64Format()
-        assertNull format.format(null)
+        assertThrows NullPointerException.class, { format.format(null) }
     }
 
 }
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy
index 17ec82d..10ddc09 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy
@@ -18,8 +18,9 @@
  */
 package org.apache.shiro.crypto.hash.format
 
-import org.apache.shiro.crypto.hash.Sha1Hash
+import org.apache.shiro.crypto.hash.Sha512Hash
 import org.junit.Test
+
 import static org.junit.Assert.*
 
 /**
@@ -72,7 +73,7 @@ class DefaultHashFormatFactoryTest {
     @Test
     void testGetInstanceWithMcfFormattedString() {
         Shiro1CryptFormat format = new Shiro1CryptFormat()
-        def formatted = format.format(new Sha1Hash("test"))
+        def formatted = format.format(new Sha512Hash("test"))
 
         def factory = new DefaultHashFormatFactory()
 
@@ -101,7 +102,7 @@ class DefaultHashFormatFactoryTest {
     void testMcfFormattedArgument() {
         def factory = new DefaultHashFormatFactory()
 
-        def hash = new Sha1Hash("test")
+        def hash = new Sha512Hash("test")
         def formatted = new Shiro1CryptFormat().format(hash)
 
         def instance = factory.getInstance(formatted)
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy
index de71cc1..eaf0ac2 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy
@@ -19,9 +19,11 @@
 package org.apache.shiro.crypto.hash.format
 
 import org.apache.shiro.crypto.hash.Hash
-import org.apache.shiro.crypto.hash.Sha1Hash
-import org.junit.Test
-import static org.junit.Assert.*
+import org.apache.shiro.crypto.hash.Sha512Hash
+import org.junit.jupiter.api.Test
+
+import static org.junit.Assert.assertEquals
+import static org.junit.Assert.assertThrows
 
 /**
  * Unit tests for the {@link HexFormat} implementation.
@@ -32,7 +34,7 @@ class HexFormatTest {
 
     @Test
     void testFormat() {
-        Hash hash = new Sha1Hash("hello");
+        Hash hash = new Sha512Hash("hello");
         HexFormat format = new HexFormat()
         String hex = format.format(hash)
         assertEquals hex, hash.toHex()
@@ -41,7 +43,7 @@ class HexFormatTest {
     @Test
     void testFormatWithNullArgument() {
         HexFormat format = new HexFormat()
-        assertNull format.format(null)
+        assertThrows NullPointerException, { format.format(null) }
     }
 
 }
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy
index 21229d8..3ad8d6a 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy
@@ -19,6 +19,7 @@
 package org.apache.shiro.crypto.hash.format
 
 import org.junit.Test
+
 import static org.junit.Assert.*
 
 /**
@@ -31,7 +32,7 @@ class ProvidedHashFormatTest {
     @Test
     void testDefaults() {
         def set = ProvidedHashFormat.values() as Set
-        assertEquals 3, set.size()
+        assertEquals 4, set.size()
         assertTrue set.contains(ProvidedHashFormat.HEX)
         assertTrue set.contains(ProvidedHashFormat.BASE64)
         assertTrue set.contains(ProvidedHashFormat.SHIRO1)
diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy
index 2b10c09..b4b38aa 100644
--- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy
+++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy
@@ -21,6 +21,7 @@ package org.apache.shiro.crypto.hash.format
 import org.apache.shiro.crypto.SecureRandomNumberGenerator
 import org.apache.shiro.crypto.hash.SimpleHash
 import org.junit.Test
+
 import static org.junit.Assert.*
 
 /**
@@ -65,7 +66,7 @@ class Shiro1CryptFormatTest {
         def rng = new SecureRandomNumberGenerator()
         def source = rng.nextBytes()
 
-        def hash = new SimpleHash(alg, source, null, iterations)
+        def hash = new SimpleHash(alg, source, iterations)
 
         String formatted = format.format(hash);
 
@@ -120,7 +121,7 @@ class Shiro1CryptFormatTest {
         def rng = new SecureRandomNumberGenerator()
         def source = rng.nextBytes()
 
-        def hash = new SimpleHash(alg, source, null, iterations)
+        def hash = new SimpleHash(alg, source, iterations)
 
         String formatted = Shiro1CryptFormat.MCF_PREFIX +
                 alg + delim +
@@ -133,7 +134,7 @@ class Shiro1CryptFormatTest {
         assertEquals hash, parsedHash
         assertEquals hash.algorithmName, parsedHash.algorithmName
         assertEquals hash.iterations, parsedHash.iterations
-        assertNull hash.salt
+        assertTrue hash.salt.isEmpty()
         assertTrue Arrays.equals(hash.bytes, parsedHash.bytes)
     }
 
diff --git a/crypto/pom.xml b/crypto/pom.xml
index b7f0e68..72dba2b 100644
--- a/crypto/pom.xml
+++ b/crypto/pom.xml
@@ -36,6 +36,7 @@
         <module>core</module>
         <module>hash</module>
         <module>cipher</module>
+        <module>support</module>
     </modules>
 
 </project>
diff --git a/crypto/cipher/pom.xml b/crypto/support/hashes/argon2/pom.xml
similarity index 53%
copy from crypto/cipher/pom.xml
copy to crypto/support/hashes/argon2/pom.xml
index 72974d4..208a054 100644
--- a/crypto/cipher/pom.xml
+++ b/crypto/support/hashes/argon2/pom.xml
@@ -17,20 +17,31 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
 
     <parent>
-        <groupId>org.apache.shiro</groupId>
-        <artifactId>shiro-crypto</artifactId>
+        <groupId>org.apache.shiro.crypto</groupId>
+        <artifactId>shiro-crypto-support</artifactId>
         <version>2.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
+        <relativePath>../../pom.xml</relativePath>
     </parent>
 
-    <modelVersion>4.0.0</modelVersion>
-    <artifactId>shiro-crypto-cipher</artifactId>
-    <name>Apache Shiro :: Cryptography :: Ciphers</name>
+    <artifactId>shiro-hashes-argon2</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support :: Hashes :: Argon2</name>
+
     <packaging>bundle</packaging>
 
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shiro</groupId>
+            <artifactId>shiro-crypto-hash</artifactId>
+        </dependency>
+    </dependencies>
+
     <build>
         <plugins>
             <plugin>
@@ -39,35 +50,30 @@
                 <extensions>true</extensions>
                 <configuration>
                     <instructions>
-                        <Bundle-SymbolicName>org.apache.shiro.crypto.cipher</Bundle-SymbolicName>
-                        <Export-Package>org.apache.shiro.crypto.cipher.*;version=${project.version}</Export-Package>
+                        <Bundle-SymbolicName>org.apache.shiro.hashes.argon2</Bundle-SymbolicName>
+                        <Export-Package>org.apache.hashes.argon2*;version=${project.version}</Export-Package>
                         <Import-Package>
-                            org.apache.shiro.crypto*;version="${shiro.osgi.importRange}",
-                            org.apache.shiro.lang*;version="${shiro.osgi.importRange}",
+                            org.apache.shiro*;version="${shiro.osgi.importRange}",
+                            org.aopalliance*;version="[1.0.0, 2.0.0)",
+                            com.google.inject*;version="1.3",
                             *
                         </Import-Package>
                     </instructions>
                 </configuration>
             </plugin>
+            <plugin>
+                <!-- Package tests so we can re-run them with guice4 -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-lang</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-crypto-core</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.64</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
 </project>
diff --git a/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java
new file mode 100644
index 0000000..fee3552
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java
@@ -0,0 +1,349 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.argon2;
+
+import org.apache.shiro.crypto.hash.AbstractCryptHash;
+import org.apache.shiro.lang.codec.Base64;
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
+import org.bouncycastle.crypto.params.Argon2Parameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64.Encoder;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.regex.Pattern;
+
+import static java.util.Collections.unmodifiableSet;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The Argon2 key derivation function (KDF) is a modern algorithm to shade and hash passwords.
+ *
+ * <p>The default implementation ({@code argon2id}) is designed to use both memory and cpu to make
+ * brute force attacks unfeasible.</p>
+ *
+ * <p>The defaults are taken from
+ * <a href="https://argon2-cffi.readthedocs.io/en/stable/parameters.html">argon2-cffi.readthedocs.io</a>.
+ * The RFC suggests to use 1 GiB of memory for frontend and 4 GiB for backend authentication.</p>
+ *
+ * <p>Example crypt string is: {@code $argon2i$v=19$m=16384,t=100,p=2$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY}.</p>
+ *
+ * @since 2.0
+ */
+class Argon2Hash extends AbstractCryptHash {
+    private static final long serialVersionUID = 2647354947284558921L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(Argon2Hash.class);
+
+    public static final String DEFAULT_ALGORITHM_NAME = "argon2id";
+
+    public static final int DEFAULT_ALGORITHM_VERSION = Argon2Parameters.ARGON2_VERSION_13;
+
+    public static final int DEFAULT_ITERATIONS = 3;
+
+    public static final int DEFAULT_MEMORY_KIB = 4096;
+
+    private static final Set<String> ALGORITHMS_ARGON2 = new HashSet<>(Arrays.asList("argon2id", "argon2i", "argon2d"));
+
+    private static final Pattern DELIMITER_COMMA = Pattern.compile(",");
+
+
+    public static final int DEFAULT_PARALLELISM = 4;
+
+    public static final int DEFAULT_OUTPUT_LENGTH = 32;
+
+
+    /**
+     * 128 bits of salt is the recommended salt length.
+     */
+    private static final int SALT_LENGTH = 16;
+
+    private final int argonVersion;
+
+    private final int iterations;
+
+    private final int memoryKiB;
+
+    private final int parallelism;
+
+    public Argon2Hash(String algorithmName, int argonVersion, byte[] hashedData, ByteSource salt, int iterations, int memoryAsKB, int parallelism) {
+        super(algorithmName, hashedData, salt);
+        this.argonVersion = argonVersion;
+        this.iterations = iterations;
+        this.memoryKiB = memoryAsKB;
+        this.parallelism = parallelism;
+
+        checkValidIterations();
+    }
+
+    public static Set<String> getAlgorithmsArgon2() {
+        return unmodifiableSet(ALGORITHMS_ARGON2);
+    }
+
+    protected static ByteSource createSalt() {
+        return createSalt(new SecureRandom());
+    }
+
+    public static ByteSource createSalt(SecureRandom random) {
+        return new SimpleByteSource(random.generateSeed(SALT_LENGTH));
+    }
+
+    public static Argon2Hash fromString(String input) {
+        // expected:
+        // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY
+        if (!input.startsWith("$")) {
+            throw new UnsupportedOperationException("Unsupported input: " + input);
+        }
+
+        final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1));
+        final String algorithmName = parts[0].trim();
+
+        if (!ALGORITHMS_ARGON2.contains(algorithmName)) {
+            throw new UnsupportedOperationException("Unsupported algorithm: " + algorithmName + ". Expected one of: " + ALGORITHMS_ARGON2);
+        }
+
+        final int version = parseVersion(parts[1]);
+        final String parameters = parts[2];
+        final int memoryPowTwo = parseMemory(parameters);
+        final int iterations = parseIterations(parameters);
+        final int parallelism = parseParallelism(parameters);
+        final ByteSource salt = new SimpleByteSource(Base64.decode(parts[3]));
+        final byte[] hashedData = Base64.decode(parts[4]);
+
+        return new Argon2Hash(algorithmName, version, hashedData, salt, iterations, memoryPowTwo, parallelism);
+    }
+
+    private static int parseParallelism(String parameters) {
+        String parameter = DELIMITER_COMMA.splitAsStream(parameters)
+                .filter(parm -> parm.startsWith("p="))
+                .findAny()
+                .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 'p='. Got: [" + parameters + "]."));
+        return Integer.parseInt(parameter.substring(2));
+    }
+
+    private static int parseIterations(String parameters) {
+        String parameter = DELIMITER_COMMA.splitAsStream(parameters)
+                .filter(parm -> parm.startsWith("t="))
+                .findAny()
+                .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 't='. Got: [" + parameters + "]."));
+
+        return Integer.parseInt(parameter.substring(2));
+    }
+
+    private static int parseMemory(String parameters) {
+        String parameter = DELIMITER_COMMA.splitAsStream(parameters)
+                .filter(parm -> parm.startsWith("m="))
+                .findAny()
+                .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 'm='. Got: [" + parameters + "]."));
+
+        return Integer.parseInt(parameter.substring(2));
+    }
+
+    private static int parseVersion(final String part) {
+        if (!part.startsWith("v=")) {
+            throw new IllegalArgumentException("Did not find version parameter 'v='. Got: [" + part + "].");
+        }
+
+        return Integer.parseInt(part.substring(2));
+    }
+
+    public static Argon2Hash generate(final char[] source) {
+        return generate(new SimpleByteSource(source), createSalt(), DEFAULT_ITERATIONS);
+    }
+
+    public static Argon2Hash generate(final ByteSource source, final ByteSource salt, final int iterations) {
+        return generate(DEFAULT_ALGORITHM_NAME, source, requireNonNull(salt, "salt"), iterations);
+    }
+
+    public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations) {
+        return generate(algorithmName, DEFAULT_ALGORITHM_VERSION, source, salt, iterations, DEFAULT_MEMORY_KIB, DEFAULT_PARALLELISM, DEFAULT_OUTPUT_LENGTH);
+    }
+
+    public static Argon2Hash generate(
+            String algorithmName,
+            int argonVersion,
+            ByteSource source,
+            ByteSource salt,
+            int iterations,
+            int memoryAsKB,
+            int parallelism,
+            int outputLength
+    ) {
+        final int type;
+        switch (requireNonNull(algorithmName, "algorithmName")) {
+            case "argon2i":
+                type = Argon2Parameters.ARGON2_i;
+                break;
+            case "argon2d":
+                type = Argon2Parameters.ARGON2_d;
+                break;
+            case "argon2":
+                // fall through
+            case "argon2id":
+                type = Argon2Parameters.ARGON2_id;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown argon2 algorithm: " + algorithmName);
+        }
+
+        final Argon2Parameters parameters = new Argon2Parameters.Builder(type)
+                .withVersion(argonVersion)
+                .withIterations(iterations)
+                .withParallelism(parallelism)
+                .withSalt(requireNonNull(salt, "salt").getBytes())
+                .withMemoryAsKB(memoryAsKB)
+                .build();
+
+        final Argon2BytesGenerator gen = new Argon2BytesGenerator();
+        gen.init(parameters);
+
+        final byte[] hash = new byte[outputLength];
+        gen.generateBytes(source.getBytes(), hash);
+
+        return new Argon2Hash(algorithmName, argonVersion, hash, new SimpleByteSource(salt), iterations, memoryAsKB, parallelism);
+    }
+
+    @Override
+    protected void checkValidAlgorithm() {
+        if (!ALGORITHMS_ARGON2.contains(getAlgorithmName())) {
+            final String message = String.format(
+                    Locale.ENGLISH,
+                    "Given algorithm name [%s] not valid for argon2. " +
+                            "Valid algorithms: [%s].",
+                    getAlgorithmName(),
+                    ALGORITHMS_ARGON2
+            );
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    protected void checkValidIterations() {
+        int iterations = this.getIterations();
+        if (iterations < 1) {
+            final String message = String.format(
+                    Locale.ENGLISH,
+                    "Expected argon2 iterations >= 1, but was [%d].",
+                    iterations
+            );
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    @Override
+    public int getIterations() {
+        return this.iterations;
+    }
+
+    @Override
+    public boolean matchesPassword(ByteSource plaintextBytes) {
+        try {
+            Argon2Hash compare = generate(this.getAlgorithmName(), this.argonVersion, plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism, this.getBytes().length);
+
+            return this.equals(compare);
+        } catch (IllegalArgumentException illegalArgumentException) {
+            // cannot recreate hash. Do not log password.
+            LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException);
+            return false;
+        }
+    }
+
+    @Override
+    public int getSaltLength() {
+        return SALT_LENGTH;
+    }
+
+    @Override
+    public String formatToCryptString() {
+        // expected:
+        // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY
+        Encoder encoder = java.util.Base64.getEncoder().withoutPadding();
+        String saltBase64 = encoder.encodeToString(this.getSalt().getBytes());
+        String dataBase64 = encoder.encodeToString(this.getBytes());
+
+        return new StringJoiner("$", "$", "")
+                .add(this.getAlgorithmName())
+                .add("v=" + this.argonVersion)
+                .add(formatParameters())
+                .add(saltBase64)
+                .add(dataBase64)
+                .toString();
+    }
+
+    private CharSequence formatParameters() {
+        return String.format(
+                Locale.ENGLISH,
+                "t=%d,m=%d,p=%d",
+                getIterations(),
+                getMemoryKiB(),
+                getParallelism()
+        );
+    }
+
+    public int getMemoryKiB() {
+        return memoryKiB;
+    }
+
+    public int getParallelism() {
+        return parallelism;
+    }
+
+    public int getArgonVersion() {
+        return argonVersion;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+        if (!super.equals(other)) {
+            return false;
+        }
+        Argon2Hash that = (Argon2Hash) other;
+        return argonVersion == that.argonVersion && iterations == that.iterations && memoryKiB == that.memoryKiB && parallelism == that.parallelism;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), argonVersion, iterations, memoryKiB, parallelism);
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", Argon2Hash.class.getSimpleName() + "[", "]")
+                .add("super=" + super.toString())
+                .add("version=" + argonVersion)
+                .add("iterations=" + iterations)
+                .add("memoryKiB=" + memoryKiB)
+                .add("parallelism=" + parallelism)
+                .toString();
+    }
+}
diff --git a/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java
new file mode 100644
index 0000000..fdacbc3
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.argon2;
+
+import org.apache.shiro.crypto.hash.HashRequest;
+import org.apache.shiro.crypto.hash.HashSpi;
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * A HashProvider for the Argon2 hash algorithm.
+ *
+ * <p>This class is intended to be used by the {@code HashProvider} class from Shiro. However,
+ * this class can also be used to created instances of the Argon2 hash manually.</p>
+ *
+ * <p>Furthermore, there is a nested {@link Parameters} class which provides names for the
+ * keys used in the parameters map of the {@link HashRequest} class.</p>
+ *
+ * @since 2.0
+ */
+public class Argon2HashProvider implements HashSpi {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class);
+
+    @Override
+    public Set<String> getImplementedAlgorithms() {
+        return Argon2Hash.getAlgorithmsArgon2();
+    }
+
+    @Override
+    public Argon2Hash fromString(String format) {
+        return Argon2Hash.fromString(format);
+    }
+
+    @Override
+    public HashFactory newHashFactory(Random random) {
+        return new Argon2HashFactory(random);
+    }
+
+    static class Argon2HashFactory implements HashSpi.HashFactory {
+
+        private final SecureRandom random;
+
+        public Argon2HashFactory(Random random) {
+            if (!(random instanceof SecureRandom)) {
+                throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!");
+            }
+
+            this.random = (SecureRandom) random;
+        }
+
+        @Override
+        public Argon2Hash generate(HashRequest hashRequest) {
+            final String algorithmName = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_NAME))
+                    .map(algo -> (String) algo)
+                    .orElse(Parameters.DEFAULT_ALGORITHM_NAME);
+
+            final int version = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_VERSION))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ALGORITHM_VERSION))
+                    .orElse(Parameters.DEFAULT_ALGORITHM_VERSION);
+
+            final ByteSource salt = parseSalt(hashRequest);
+
+            final int iterations = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ITERATIONS))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ITERATIONS))
+                    .orElse(Parameters.DEFAULT_ITERATIONS);
+
+            final int memoryKib = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_MEMORY_KIB))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_MEMORY_KIB))
+                    .orElse(Parameters.DEFAULT_MEMORY_KIB);
+
+            final int parallelism = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_PARALLELISM))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_PARALLELISM))
+                    .orElse(Parameters.DEFAULT_PARALLELISM);
+
+            final int outputLength = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_OUTPUT_LENGTH))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_OUTPUT_LENGTH))
+                    .orElse(Parameters.DEFAULT_OUTPUT_LENGTH);
+
+            return Argon2Hash.generate(
+                    algorithmName,
+                    version,
+                    hashRequest.getSource(),
+                    salt,
+                    iterations,
+                    memoryKib,
+                    parallelism,
+                    outputLength
+            );
+        }
+
+        private ByteSource parseSalt(HashRequest hashRequest) {
+            return Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_SALT))
+                    .map(saltParm -> Base64.getDecoder().decode((String) saltParm))
+                    .map(SimpleByteSource::new)
+                    .flatMap(this::lengthValidOrEmpty)
+                    .orElseGet(() -> Argon2Hash.createSalt(random));
+        }
+
+        private Optional<ByteSource> lengthValidOrEmpty(ByteSource bytes) {
+            if (bytes.getBytes().length != 16) {
+                return Optional.empty();
+            }
+
+            return Optional.of(bytes);
+        }
+
+        private Optional<Integer> intOrEmpty(Object maybeInt, String parameterName) {
+            try {
+                return Optional.of(Integer.parseInt((String) maybeInt, 10));
+            } catch (NumberFormatException numberFormatException) {
+                String message = String.format(
+                        Locale.ENGLISH,
+                        "Expected Integer for parameter %s, but %s is not parsable.",
+                        parameterName, maybeInt
+                );
+                LOG.warn(message, numberFormatException);
+                return Optional.empty();
+            }
+        }
+    }
+
+    /**
+     * Parameters for the {@link Argon2Hash} class.
+     *
+     * <p>This class contains public constants only. The constants starting with {@code PARAMETER_} are
+     * the parameter names recognized by the
+     * {@link org.apache.shiro.crypto.hash.HashSpi.HashFactory#generate(HashRequest)} method.</p>
+     *
+     * <p>The constants starting with {@code DEFAULT_} are their respective default values.</p>
+     */
+    public static final class Parameters {
+
+        public static final String DEFAULT_ALGORITHM_NAME = Argon2Hash.DEFAULT_ALGORITHM_NAME;
+        public static final int DEFAULT_ALGORITHM_VERSION = Argon2Hash.DEFAULT_ALGORITHM_VERSION;
+        public static final int DEFAULT_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS;
+        public static final int DEFAULT_MEMORY_KIB = Argon2Hash.DEFAULT_MEMORY_KIB;
+        public static final int DEFAULT_PARALLELISM = Argon2Hash.DEFAULT_PARALLELISM;
+        public static final int DEFAULT_OUTPUT_LENGTH = Argon2Hash.DEFAULT_OUTPUT_LENGTH;
+
+        /**
+         * Parameter for modifying the internal algorithm used by Argon2.
+         *
+         * <p>Valid values are {@code argon2i} (optimized to resist side-channel attacks),
+         * {@code argon2d} (maximizes resistance to GPU cracking attacks)
+         * and {@code argon2id} (a hybrid version).</p>
+         *
+         * <p>The default value is {@value DEFAULT_ALGORITHM_NAME} when this parameter is not specified.</p>
+         */
+        public static final String PARAMETER_ALGORITHM_NAME = "Argon2.algorithmName";
+        public static final String PARAMETER_ALGORITHM_VERSION = "Argon2.version";
+
+        /**
+         * The salt to use.
+         *
+         * <p>The value for this parameter accepts a Base64-encoded 16byte (128bit) salt.</p>
+         *
+         * <p>As for any KDF, do not use a static salt value for multiple passwords.</p>
+         *
+         * <p>The default value is a new random 128bit-salt, if this parameter is not specified.</p>
+         */
+        public static final String PARAMETER_SALT = "Argon2.salt";
+
+        public static final String PARAMETER_ITERATIONS = "Argon2.iterations";
+        public static final String PARAMETER_MEMORY_KIB = "Argon2.memoryKib";
+        public static final String PARAMETER_PARALLELISM = "Argon2.parallelism";
+
+        /**
+         * The output length of the resulting data section.
+         *
+         * <p>Argon2 allows to modify the length of the generated output.</p>
+         *
+         * <p>The default value is {@value DEFAULT_OUTPUT_LENGTH} when this parameter is not specified.</p>
+         */
+        public static final String PARAMETER_OUTPUT_LENGTH = "Argon2.outputLength";
+
+        private Parameters() {
+            // utility class
+        }
+    }
+}
diff --git a/crypto/hash/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
similarity index 78%
copy from crypto/hash/src/main/resources/META-INF/NOTICE
copy to crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
index 9d26a95..4b3b138 100644
--- a/crypto/hash/src/main/resources/META-INF/NOTICE
+++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
@@ -4,10 +4,13 @@ Copyright 2008-2020 The Apache Software Foundation
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
 
-The implementation for org.apache.shiro.util.SoftHashMap is based 
-on initial ideas from Dr. Heinz Kabutz's publicly posted version 
+The implementation for org.apache.shiro.util.SoftHashMap is based
+on initial ideas from Dr. Heinz Kabutz's publicly posted version
 available at http://www.javaspecialists.eu/archive/Issue015.html,
-with continued modifications.  
+with continued modifications.
+
+The implementation of Radix64 aka BcryptBase64 (OpenBSD BCrypt’s Base64) was copied
+from https://github.com/patrickfav/bcrypt.
 
 Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
 code for this  product was copied for simplicity and to reduce
diff --git a/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
new file mode 100644
index 0000000..80a9e65
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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
+#
+#     http://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.
+#
+
+org.apache.shiro.crypto.support.hashes.argon2.Argon2HashProvider
diff --git a/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy
new file mode 100644
index 0000000..638fa8d
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.argon2
+
+import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
+import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat
+import org.apache.shiro.lang.util.SimpleByteSource
+import org.bouncycastle.crypto.params.Argon2Parameters
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.function.Executable
+
+import static org.junit.jupiter.api.Assertions.*
+
+class Argon2HashTest {
+
+    private static final TEST_PASSWORD = "secret#shiro,password;Jo8opech";
+    private static final TEST_PASSWORD_BS = new SimpleByteSource(TEST_PASSWORD)
+
+    @Test
+    void testArgon2Hash() {
+        // given
+        def shiro2Format = '$shiro2$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI'
+        def expectedPassword = new SimpleByteSource('secret#shiro,password;Jo8opech')
+
+        // when
+        def hash = new Shiro2CryptFormat().parse(shiro2Format) as Argon2Hash;
+        System.out.println("Hash: " + hash)
+        def matchesPassword = hash.matchesPassword expectedPassword;
+
+        // then
+        assertEquals Argon2Parameters.ARGON2_VERSION_13, hash.argonVersion
+        assertEquals 3, hash.iterations
+        assertEquals 4096, hash.memoryKiB
+        assertEquals 4, hash.parallelism
+        assertTrue matchesPassword
+    }
+
+    @Test
+    void testArgon2HashShiro1Format() {
+        // given
+        def shiro1Format = '$shiro1$argon2id$v=19$t=2,m=131072,p=4$7858qTJTreh61AzFV2XMOw==$lLzl2VNNbyFcuJo0Hp7JQpguKCDoQwxo91AWobcHzeo='
+
+        // when
+        def thrownException = assertThrows(
+                UnsupportedOperationException,
+                { new Shiro1CryptFormat().parse shiro1Format } as Executable
+        )
+
+        // then
+        assertTrue thrownException.getMessage().contains("shiro1")
+    }
+
+    @Test
+    void testFromStringMatchesPw() {
+        // when
+        def argon2String = '$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI'
+        // for testing recreated salt and data parts, as the parameter order could change.
+        def saltDataPart = argon2String.substring(30)
+
+        // when
+        def argon2Hash = Argon2Hash.fromString argon2String
+        def recreatedSaltDataPart = argon2Hash.formatToCryptString().substring(30)
+
+        // then
+        assertTrue argon2Hash.matchesPassword(TEST_PASSWORD_BS)
+        // we can only test the salt + data parts, as
+        // the parameter order could change.
+        assertEquals saltDataPart, recreatedSaltDataPart
+    }
+
+}
diff --git a/crypto/cipher/pom.xml b/crypto/support/hashes/bcrypt/pom.xml
similarity index 53%
copy from crypto/cipher/pom.xml
copy to crypto/support/hashes/bcrypt/pom.xml
index 72974d4..24924d7 100644
--- a/crypto/cipher/pom.xml
+++ b/crypto/support/hashes/bcrypt/pom.xml
@@ -17,20 +17,31 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
 
     <parent>
-        <groupId>org.apache.shiro</groupId>
-        <artifactId>shiro-crypto</artifactId>
+        <groupId>org.apache.shiro.crypto</groupId>
+        <artifactId>shiro-crypto-support</artifactId>
         <version>2.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
+        <relativePath>../../pom.xml</relativePath>
     </parent>
 
-    <modelVersion>4.0.0</modelVersion>
-    <artifactId>shiro-crypto-cipher</artifactId>
-    <name>Apache Shiro :: Cryptography :: Ciphers</name>
+    <artifactId>shiro-hashes-bcrypt</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support :: Hashes :: BCrypt</name>
+
     <packaging>bundle</packaging>
 
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shiro</groupId>
+            <artifactId>shiro-crypto-hash</artifactId>
+        </dependency>
+    </dependencies>
+
     <build>
         <plugins>
             <plugin>
@@ -39,35 +50,30 @@
                 <extensions>true</extensions>
                 <configuration>
                     <instructions>
-                        <Bundle-SymbolicName>org.apache.shiro.crypto.cipher</Bundle-SymbolicName>
-                        <Export-Package>org.apache.shiro.crypto.cipher.*;version=${project.version}</Export-Package>
+                        <Bundle-SymbolicName>org.apache.shiro.hashes.bcrypt</Bundle-SymbolicName>
+                        <Export-Package>org.apache.hashes.bcrypt*;version=${project.version}</Export-Package>
                         <Import-Package>
-                            org.apache.shiro.crypto*;version="${shiro.osgi.importRange}",
-                            org.apache.shiro.lang*;version="${shiro.osgi.importRange}",
+                            org.apache.shiro*;version="${shiro.osgi.importRange}",
+                            org.aopalliance*;version="[1.0.0, 2.0.0)",
+                            com.google.inject*;version="1.3",
                             *
                         </Import-Package>
                     </instructions>
                 </configuration>
             </plugin>
+            <plugin>
+                <!-- Package tests so we can re-run them with guice4 -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-lang</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-crypto-core</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.64</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
 </project>
diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java
new file mode 100644
index 0000000..f73b40a
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.bcrypt;
+
+import org.apache.shiro.crypto.hash.AbstractCryptHash;
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import static java.util.Collections.unmodifiableSet;
+
+/**
+ * @since 2.0
+ */
+class BCryptHash extends AbstractCryptHash {
+
+    private static final long serialVersionUID = 6957869292324606101L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractCryptHash.class);
+
+    public static final String DEFAULT_ALGORITHM_NAME = "2y";
+
+    public static final int DEFAULT_COST = 10;
+
+    public static final int SALT_LENGTH = 16;
+
+    private static final Set<String> ALGORITHMS_BCRYPT = new HashSet<>(Arrays.asList("2", "2a", "2b", "2y"));
+
+    private final int cost;
+
+    private final int iterations;
+
+    public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) {
+        super(version, hashedData, salt);
+        this.cost = cost;
+        this.iterations = (int) Math.pow(2, cost);
+        checkValidCost();
+    }
+
+    @Override
+    protected final void checkValidAlgorithm() {
+        if (!ALGORITHMS_BCRYPT.contains(getAlgorithmName())) {
+            final String message = String.format(
+                    Locale.ENGLISH,
+                    "Given algorithm name [%s] not valid for bcrypt. " +
+                            "Valid algorithms: [%s].",
+                    getAlgorithmName(),
+                    ALGORITHMS_BCRYPT
+            );
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    protected final void checkValidCost() {
+        checkValidCost(this.cost);
+    }
+
+    public static int checkValidCost(final int cost) {
+        if (cost < 4 || cost > 31) {
+            final String message = String.format(
+                    Locale.ENGLISH,
+                    "Expected bcrypt cost >= 4 and <=30, but was [%d].",
+                    cost
+            );
+            throw new IllegalArgumentException(message);
+        }
+
+        return cost;
+    }
+
+    public int getCost() {
+        return this.cost;
+    }
+
+    public static Set<String> getAlgorithmsBcrypt() {
+        return unmodifiableSet(ALGORITHMS_BCRYPT);
+    }
+
+    public static BCryptHash fromString(String input) {
+        // the input string should look like this:
+        // $2y$cost$salt{22}hash
+        if (!input.startsWith("$")) {
+            throw new IllegalArgumentException("Unsupported input: " + input);
+        }
+
+        final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1));
+
+        if (parts.length != 3) {
+            throw new IllegalArgumentException("Expected string containing three '$' but got: '" + Arrays.toString(parts) + "'.");
+        }
+        final String algorithmName = parts[0].trim();
+        final int cost = Integer.parseInt(parts[1].trim(), 10);
+
+        final String dataSection = parts[2];
+        final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default();
+
+        final String saltBase64 = dataSection.substring(0, 22);
+        final String bytesBase64 = dataSection.substring(22);
+        final byte[] salt = bcryptBase64.decode(saltBase64.getBytes(StandardCharsets.ISO_8859_1));
+        final byte[] hashedData = bcryptBase64.decode(bytesBase64.getBytes(StandardCharsets.ISO_8859_1));
+
+        return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), cost);
+    }
+
+    public static BCryptHash generate(final ByteSource source) {
+        return generate(source, createSalt(), DEFAULT_COST);
+    }
+
+
+    public static BCryptHash generate(final ByteSource source, final ByteSource initialSalt, final int cost) {
+        return generate(DEFAULT_ALGORITHM_NAME, source, initialSalt, cost);
+    }
+
+    public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) {
+        checkValidCost(cost);
+        final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost);
+
+        return fromString(cryptString);
+    }
+
+    protected static ByteSource createSalt() {
+        return createSalt(new SecureRandom());
+    }
+
+    protected static ByteSource createSalt(SecureRandom random) {
+        return new SimpleByteSource(random.generateSeed(SALT_LENGTH));
+    }
+
+    @Override
+    public int getSaltLength() {
+        return SALT_LENGTH;
+    }
+
+    @Override
+    public String formatToCryptString() {
+        OpenBSDBase64.Default bsdBase64 = new OpenBSDBase64.Default();
+        String saltBase64 = new String(bsdBase64.encode(this.getSalt().getBytes()), StandardCharsets.ISO_8859_1);
+        String dataBase64 = new String(bsdBase64.encode(this.getBytes()), StandardCharsets.ISO_8859_1);
+
+        return new StringJoiner("$", "$", "")
+                .add(this.getAlgorithmName())
+                .add("" + this.cost)
+                .add(saltBase64 + dataBase64)
+                .toString();
+    }
+
+    @Override
+    public int getIterations() {
+        return this.iterations;
+    }
+
+    @Override
+    public boolean matchesPassword(ByteSource plaintextBytes) {
+        try {
+            final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost());
+            BCryptHash other = fromString(cryptString);
+
+            return this.equals(other);
+        } catch (IllegalArgumentException illegalArgumentException) {
+            // cannot recreate hash. Do not log password.
+            LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException);
+            return false;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]")
+                .add("super=" + super.toString())
+                .add("cost=" + this.cost)
+                .toString();
+    }
+}
diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java
new file mode 100644
index 0000000..7496156
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.bcrypt;
+
+import org.apache.shiro.crypto.hash.HashRequest;
+import org.apache.shiro.crypto.hash.HashSpi;
+import org.apache.shiro.lang.util.ByteSource;
+import org.apache.shiro.lang.util.SimpleByteSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * @since 2.0
+ */
+public class BCryptProvider implements HashSpi {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.class);
+
+    @Override
+    public Set<String> getImplementedAlgorithms() {
+        return BCryptHash.getAlgorithmsBcrypt();
+    }
+
+    @Override
+    public BCryptHash fromString(String format) {
+        return BCryptHash.fromString(format);
+    }
+
+    @Override
+    public HashFactory newHashFactory(Random random) {
+        return new BCryptHashFactory(random);
+    }
+
+    static class BCryptHashFactory implements HashSpi.HashFactory {
+
+        private final SecureRandom random;
+
+        public BCryptHashFactory(Random random) {
+            if (!(random instanceof SecureRandom)) {
+                throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!");
+            }
+
+            this.random = (SecureRandom) random;
+        }
+
+        @Override
+        public BCryptHash generate(HashRequest hashRequest) {
+            final String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM_NAME);
+
+            final ByteSource salt = getSalt(hashRequest);
+
+            final int cost = getCost(hashRequest);
+
+            return BCryptHash.generate(
+                    algorithmName,
+                    hashRequest.getSource(),
+                    salt,
+                    cost
+            );
+        }
+
+        private int getCost(HashRequest hashRequest) {
+            final Map<String, Object> parameters = hashRequest.getParameters();
+            final Optional<String> optCostStr = Optional.ofNullable(parameters.get(Parameters.PARAMETER_COST))
+                    .map(obj -> (String) obj);
+
+            if (!optCostStr.isPresent()) {
+                return BCryptHash.DEFAULT_COST;
+            }
+
+            String costStr = optCostStr.orElseThrow(NoSuchElementException::new);
+            try {
+                int cost = Integer.parseInt(costStr, 10);
+                BCryptHash.checkValidCost(cost);
+                return cost;
+            } catch (IllegalArgumentException costEx) {
+                String message = String.format(
+                        Locale.ENGLISH,
+                        "Expected Integer for parameter %s, but %s is not parsable or valid.",
+                        Parameters.PARAMETER_COST, costStr
+                );
+                LOG.warn(message, costEx);
+
+                return BCryptHash.DEFAULT_COST;
+            }
+        }
+
+        private ByteSource getSalt(HashRequest hashRequest) {
+            final Map<String, Object> parameters = hashRequest.getParameters();
+            final Optional<String> optSaltBase64 = Optional.ofNullable(parameters.get(Parameters.PARAMETER_SALT))
+                    .map(obj -> (String) obj);
+
+            if (!optSaltBase64.isPresent()) {
+                return BCryptHash.createSalt(random);
+            }
+
+            final String saltBase64 = optSaltBase64.orElseThrow(NoSuchElementException::new);
+            final byte[] saltBytes = Base64.getDecoder().decode(saltBase64);
+
+            if (saltBytes.length != BCryptHash.SALT_LENGTH) {
+                return BCryptHash.createSalt(random);
+            }
+
+            return new SimpleByteSource(saltBytes);
+        }
+    }
+
+    public static final class Parameters {
+        public static final String DEFAULT_ALGORITHM_NAME = BCryptHash.DEFAULT_ALGORITHM_NAME;
+
+        public static final String PARAMETER_SALT = "BCrypt.salt";
+        public static final String PARAMETER_COST = "BCrypt.cost";
+
+        private Parameters() {
+            // utility class
+        }
+    }
+}
diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java
new file mode 100644
index 0000000..ad05fe8
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.bcrypt;
+
+
+/**
+ * Encoder for the custom Base64 variant of BCrypt (called Radix64 here). It has the same rules as Base64 but uses a
+ * different mapping table than the various RFCs
+ * <p>
+ * According to Wikipedia:
+ *
+ * <blockquote>
+ * Unix stores password hashes computed with crypt in the /etc/passwd file using radix-64 encoding called B64. It uses a
+ * mostly-alphanumeric set of characters, plus . and /. Its 64-character set is "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".
+ * Padding is not used.
+ * </blockquote>
+ *
+ * @since 2.0
+ */
+interface OpenBSDBase64 {
+
+
+    /**
+     * Encode given raw byte array to a Radix64 style, UTF-8 encoded byte array.
+     *
+     * @param rawBytes to encode
+     * @return UTF-8 encoded string representing radix64 encoded data
+     */
+    byte[] encode(byte[] rawBytes);
+
+    /**
+     * From a UTF-8 encoded string representing radix64 encoded data as byte array, decodes the raw bytes from it.
+     *
+     * @param utf8EncodedRadix64String from a string get it with <code>"m0CrhHm10qJ3lXRY.5zDGO".getBytes(StandardCharsets.UTF8)</code>
+     * @return the raw bytes encoded by this utf-8 radix4 string
+     */
+    byte[] decode(byte[] utf8EncodedRadix64String);
+
+    /**
+     * A mod of Square's Okio Base64 encoder
+     * <p>
+     * Original author: Alexander Y. Kleymenov
+     *
+     * @see <a href="https://github.com/square/okio/blob/okio-parent-1.15.0/okio/src/main/java/okio/Base64.java">Okio</a>
+     */
+    class Default implements OpenBSDBase64 {
+        private static final byte[] DECODE_TABLE = {
+                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57,
+                58, 59, 60, 61, 62, 63, -1, -1, -1, -2, -1, -1, -1, 2, 3, 4, 5, 6, 7,
+                8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
+                26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
+                38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53};
+
+        private static final byte[] MAP = new byte[]{
+                '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+                'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+                'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
+                'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+                'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
+                '6', '7', '8', '9'
+        };
+
+        @Override
+        public byte[] encode(final byte[] in) {
+            return encode(in, MAP);
+        }
+
+        @Override
+        public byte[] decode(final byte[] in) {
+            // Ignore trailing '=' padding and whitespace from the input.
+            int limit = in.length;
+            for (; limit > 0; limit--) {
+                final byte c = in[limit - 1];
+                if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') {
+                    break;
+                }
+            }
+
+            // If the input includes whitespace, this output array will be longer than necessary.
+            final byte[] out = new byte[(int) (limit * 6L / 8L)];
+            int outCount = 0;
+            int inCount = 0;
+
+            int word = 0;
+            for (int pos = 0; pos < limit; pos++) {
+                final byte c = in[pos];
+
+                final int bits;
+                if (c == '.' || c == '/' || (c >= 'A' && c <= 'z') || (c >= '0' && c <= '9')) {
+                    bits = DECODE_TABLE[c];
+                } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
+                    continue;
+                } else {
+                    throw new IllegalArgumentException("invalid character to decode: " + c);
+                }
+
+                // Append this char's 6 bits to the word.
+                word = (word << 6) | (byte) bits;
+
+                // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
+                inCount++;
+                if (inCount % 4 == 0) {
+                    out[outCount++] = (byte) (word >> 16);
+                    out[outCount++] = (byte) (word >> 8);
+                    out[outCount++] = (byte) word;
+                }
+            }
+
+            final int lastWordChars = inCount % 4;
+            if (lastWordChars == 1) {
+                // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
+                return new byte[0];
+            } else if (lastWordChars == 2) {
+                // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
+                word = word << 12;
+                out[outCount++] = (byte) (word >> 16);
+            } else if (lastWordChars == 3) {
+                // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
+                word = word << 6;
+                out[outCount++] = (byte) (word >> 16);
+                out[outCount++] = (byte) (word >> 8);
+            }
+
+            // If we sized our out array perfectly, we're done.
+            if (outCount == out.length) {
+                return out;
+            }
+
+            // Copy the decoded bytes to a new, right-sized array.
+            final byte[] prefix = new byte[outCount];
+            System.arraycopy(out, 0, prefix, 0, outCount);
+            return prefix;
+        }
+
+        private static byte[] encode(final byte[] in, final byte[] map) {
+            final int length = 4 * (in.length / 3) + (in.length % 3 == 0 ? 0 : in.length % 3 + 1);
+            final byte[] out = new byte[length];
+            int index = 0;
+            final int end = in.length - in.length % 3;
+            for (int i = 0; i < end; i += 3) {
+                out[index++] = map[(in[i] & 0xff) >> 2];
+                out[index++] = map[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
+                out[index++] = map[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
+                out[index++] = map[(in[i + 2] & 0x3f)];
+            }
+            switch (in.length % 3) {
+                case 1:
+                    out[index++] = map[(in[end] & 0xff) >> 2];
+                    out[index] = map[(in[end] & 0x03) << 4];
+                    break;
+                case 2:
+                    out[index++] = map[(in[end] & 0xff) >> 2];
+                    out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
+                    out[index] = map[((in[end + 1] & 0x0f) << 2)];
+                    break;
+            }
+            return out;
+        }
+    }
+}
diff --git a/crypto/hash/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
similarity index 78%
copy from crypto/hash/src/main/resources/META-INF/NOTICE
copy to crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
index 9d26a95..4b3b138 100644
--- a/crypto/hash/src/main/resources/META-INF/NOTICE
+++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
@@ -4,10 +4,13 @@ Copyright 2008-2020 The Apache Software Foundation
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
 
-The implementation for org.apache.shiro.util.SoftHashMap is based 
-on initial ideas from Dr. Heinz Kabutz's publicly posted version 
+The implementation for org.apache.shiro.util.SoftHashMap is based
+on initial ideas from Dr. Heinz Kabutz's publicly posted version
 available at http://www.javaspecialists.eu/archive/Issue015.html,
-with continued modifications.  
+with continued modifications.
+
+The implementation of Radix64 aka BcryptBase64 (OpenBSD BCrypt’s Base64) was copied
+from https://github.com/patrickfav/bcrypt.
 
 Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
 code for this  product was copied for simplicity and to reduce
diff --git a/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
new file mode 100644
index 0000000..95d1df3
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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
+#
+#     http://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.
+#
+
+org.apache.shiro.crypto.support.hashes.bcrypt.BCryptProvider
diff --git a/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy
new file mode 100644
index 0000000..f95e1a2
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.crypto.support.hashes.bcrypt
+
+import org.apache.shiro.lang.util.SimpleByteSource
+import org.junit.jupiter.api.Test
+
+import java.nio.charset.StandardCharsets
+import java.security.SecureRandom
+
+import static java.lang.Math.pow
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+/**
+ * @since 2.0
+ */
+class BCryptHashTest {
+
+    private static final String TEST_PASSWORD = "secret#shiro,password;Jo8opech";
+
+    @Test
+    void testCreateHashGenerateSaltIterations() {
+        // given
+        final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD)
+
+        // when
+        final def bCryptHash = BCryptHash.generate testPasswordChars;
+
+        // then
+        assertEquals BCryptHash.DEFAULT_COST, bCryptHash.cost;
+    }
+
+    @Test
+    void testCreateHashGivenSalt() {
+        // given
+        final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD);
+        final def salt = new SimpleByteSource(new SecureRandom().generateSeed(16))
+        final def cost = 6
+
+        // when
+        final def bCryptHash = BCryptHash.generate(testPasswordChars, salt, cost);
+
+        // then
+        assertEquals cost, bCryptHash.cost;
+        assertEquals pow(2, cost) as int, bCryptHash.iterations;
+        assertEquals salt, bCryptHash.salt;
+    }
+
+    @Test
+    void toBase64EqualsInput() {
+        // given
+        def salt = '7rOjsAf2U/AKKqpMpCIn6e'
+        def saltBytes = new SimpleByteSource(new OpenBSDBase64.Default().decode(salt.getBytes(StandardCharsets.ISO_8859_1)))
+        def testPwBytes = new SimpleByteSource(TEST_PASSWORD)
+        def expectedHashString = '$2y$10$' + salt + 'tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.'
+
+
+        // when
+        def bCryptHash = BCryptHash.generate("2y", testPwBytes, saltBytes, 10)
+
+        // then
+        assertEquals expectedHashString, bCryptHash.formatToCryptString()
+    }
+
+    @Test
+    void testMatchesPassword() {
+        // given
+        def expectedHashString = '$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.'
+        def bCryptHash = BCryptHash.fromString(expectedHashString)
+        def testPwBytes = new SimpleByteSource(TEST_PASSWORD)
+
+        // when
+        def matchesPassword = bCryptHash.matchesPassword testPwBytes
+
+
+        // then
+        assertTrue matchesPassword
+    }
+
+}
diff --git a/crypto/pom.xml b/crypto/support/pom.xml
similarity index 69%
copy from crypto/pom.xml
copy to crypto/support/pom.xml
index b7f0e68..582fe24 100644
--- a/crypto/pom.xml
+++ b/crypto/support/pom.xml
@@ -17,26 +17,28 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
 
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <parent>
         <groupId>org.apache.shiro</groupId>
-        <artifactId>shiro-root</artifactId>
+        <artifactId>shiro-crypto</artifactId>
         <version>2.0.0-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
-    <artifactId>shiro-crypto</artifactId>
-    <name>Apache Shiro :: Cryptography</name>
+    <groupId>org.apache.shiro.crypto</groupId>
+    <artifactId>shiro-crypto-support</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support</name>
     <packaging>pom</packaging>
 
     <modules>
-        <module>core</module>
-        <module>hash</module>
-        <module>cipher</module>
+        <module>hashes/argon2</module>
+        <module>hashes/bcrypt</module>
     </modules>
 
-</project>
 
+</project>
diff --git a/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java b/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java
index e503f7e..d7fd0c8 100644
--- a/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java
+++ b/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java
@@ -20,7 +20,13 @@ package org.apache.shiro.lang.codec;
 
 import org.apache.shiro.lang.util.ByteSource;
 
-import java.io.*;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 
 /**
  * Base abstract class that provides useful encoding and decoding operations, especially for character data.
@@ -188,28 +194,28 @@ public abstract class CodecSupport {
      * If the argument is anything other than these types, it is passed to the
      * {@link #objectToBytes(Object) objectToBytes} method which must be overridden by subclasses.
      *
-     * @param o the Object to convert into a byte array
+     * @param object the Object to convert into a byte array
      * @return a byte array representation of the Object argument.
      */
-    protected byte[] toBytes(Object o) {
-        if (o == null) {
+    protected byte[] toBytes(Object object) {
+        if (object == null) {
             String msg = "Argument for byte conversion cannot be null.";
             throw new IllegalArgumentException(msg);
         }
-        if (o instanceof byte[]) {
-            return (byte[]) o;
-        } else if (o instanceof ByteSource) {
-            return ((ByteSource) o).getBytes();
-        } else if (o instanceof char[]) {
-            return toBytes((char[]) o);
-        } else if (o instanceof String) {
-            return toBytes((String) o);
-        } else if (o instanceof File) {
-            return toBytes((File) o);
-        } else if (o instanceof InputStream) {
-            return toBytes((InputStream) o);
+        if (object instanceof byte[]) {
+            return (byte[]) object;
+        } else if (object instanceof ByteSource) {
+            return ((ByteSource) object).getBytes();
+        } else if (object instanceof char[]) {
+            return toBytes((char[]) object);
+        } else if (object instanceof String) {
+            return toBytes((String) object);
+        } else if (object instanceof File) {
+            return toBytes((File) object);
+        } else if (object instanceof InputStream) {
+            return toBytes((InputStream) object);
         } else {
-            return objectToBytes(o);
+            return objectToBytes(object);
         }
     }
 
diff --git a/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java b/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java
index 18594f6..dbb8d3d 100644
--- a/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java
+++ b/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java
@@ -130,14 +130,21 @@ public class SimpleByteSource implements ByteSource {
                 o instanceof ByteSource || o instanceof File || o instanceof InputStream;
     }
 
+    public static ByteSource empty() {
+        return new SimpleByteSource(new byte[]{});
+    }
+
+    @Override
     public byte[] getBytes() {
-        return this.bytes;
+        return Arrays.copyOf(this.bytes, this.bytes.length);
     }
 
+    @Override
     public boolean isEmpty() {
         return this.bytes == null || this.bytes.length == 0;
     }
 
+    @Override
     public String toHex() {
         if ( this.cachedHex == null ) {
             this.cachedHex = Hex.encodeToString(getBytes());
@@ -145,6 +152,7 @@ public class SimpleByteSource implements ByteSource {
         return this.cachedHex;
     }
 
+    @Override
     public String toBase64() {
         if ( this.cachedBase64 == null ) {
             this.cachedBase64 = Base64.encodeToString(getBytes());
@@ -152,10 +160,12 @@ public class SimpleByteSource implements ByteSource {
         return this.cachedBase64;
     }
 
+    @Override
     public String toString() {
         return toBase64();
     }
 
+    @Override
     public int hashCode() {
         if (this.bytes == null || this.bytes.length == 0) {
             return 0;
@@ -163,6 +173,7 @@ public class SimpleByteSource implements ByteSource {
         return Arrays.hashCode(this.bytes);
     }
 
+    @Override
     public boolean equals(Object o) {
         if (o == this) {
             return true;
diff --git a/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java
index 4ae65c9..b748d32 100644
--- a/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java
+++ b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java
@@ -322,6 +322,18 @@ public class StringUtils {
         return split;
     }
 
+    /**
+     * Splits a string using the {@link #DEFAULT_DELIMITER_CHAR} (which is {@value #DEFAULT_DELIMITER_CHAR}).
+     * This method also recognizes quoting using the {@link #DEFAULT_QUOTE_CHAR}
+     * (which is {@value #DEFAULT_QUOTE_CHAR}), but does not retain them.
+     * 
+     * <p>This is equivalent of calling {@link #split(String, char, char, char, boolean, boolean)} with
+     * {@code line, DEFAULT_DELIMITER_CHAR, DEFAULT_QUOTE_CHAR, DEFAULT_QUOTE_CHAR, false, true}.</p>
+     * 
+     * @param line the line to split using the {@link #DEFAULT_DELIMITER_CHAR}.
+     * @return the split line, split tokens do not contain quotes and are trimmed.
+     * @see #split(String, char, char, char, boolean, boolean)
+     */
     public static String[] split(String line) {
         return split(line, DEFAULT_DELIMITER_CHAR);
     }
diff --git a/pom.xml b/pom.xml
index 41d0981..1ca995b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,7 @@
         <guice.version>4.2.2</guice.version>
         <jaxrs.api.version>2.1.6</jaxrs.api.version>
         <htmlunit.version>2.39.0</htmlunit.version>
+        <bouncycastle.version>1.68</bouncycastle.version>
 
         <!-- Test 3rd-party dependencies: -->
         <easymock.version>4.0.2</easymock.version>
@@ -751,6 +752,16 @@
                 <version>${project.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.apache.shiro.crypto</groupId>
+                <artifactId>shiro-hashes-argon2</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.shiro.crypto</groupId>
+                <artifactId>shiro-hashes-bcrypt</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.apache.shiro</groupId>
                 <artifactId>shiro-crypto-cipher</artifactId>
                 <version>${project.version}</version>
@@ -1223,6 +1234,12 @@
                 <artifactId>junit-servers-jetty</artifactId>
                 <version>${junit.server.jetty.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>org.bouncycastle</groupId>
+                <artifactId>bcprov-jdk15on</artifactId>
+                <version>${bouncycastle.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
     
diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml
index 9af02f8..2a3b4aa 100644
--- a/tools/hasher/pom.xml
+++ b/tools/hasher/pom.xml
@@ -44,13 +44,28 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- explicitly use the compile scopes for the algorithms, so we can access the parameter names. -->
+        <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-argon2</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-bcrypt</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <dependency>
             <groupId>commons-cli</groupId>
             <artifactId>commons-cli</artifactId>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
             <scope>runtime</scope>
         </dependency>
     </dependencies>
diff --git a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
index e203153..020d6d4 100644
--- a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
+++ b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
@@ -20,13 +20,11 @@ package org.apache.shiro.tools.hasher;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
 import org.apache.commons.cli.HelpFormatter;
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
-import org.apache.commons.cli.DefaultParser;
 import org.apache.shiro.authc.credential.DefaultPasswordService;
-import org.apache.shiro.lang.codec.Base64;
-import org.apache.shiro.lang.codec.Hex;
 import org.apache.shiro.crypto.SecureRandomNumberGenerator;
 import org.apache.shiro.crypto.UnknownAlgorithmException;
 import org.apache.shiro.crypto.hash.DefaultHashService;
@@ -37,15 +35,24 @@ import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory;
 import org.apache.shiro.crypto.hash.format.HashFormat;
 import org.apache.shiro.crypto.hash.format.HashFormatFactory;
 import org.apache.shiro.crypto.hash.format.HexFormat;
-import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
+import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat;
+import org.apache.shiro.crypto.support.hashes.argon2.Argon2HashProvider;
+import org.apache.shiro.lang.codec.Base64;
+import org.apache.shiro.lang.codec.Hex;
 import org.apache.shiro.lang.io.ResourceUtils;
 import org.apache.shiro.lang.util.ByteSource;
 import org.apache.shiro.lang.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.util.Arrays;
 
+import static java.util.Collections.emptyMap;
+
 /**
  * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc).
  * <p/>
@@ -59,16 +66,18 @@ import java.util.Arrays;
  */
 public final class Hasher {
 
+    private static final Logger LOG = LoggerFactory.getLogger(Hasher.class);
+
     private static final String HEX_PREFIX = "0x";
     private static final String DEFAULT_ALGORITHM_NAME = "MD5";
     private static final String DEFAULT_PASSWORD_ALGORITHM_NAME = DefaultPasswordService.DEFAULT_HASH_ALGORITHM;
     private static final int DEFAULT_GENERATED_SALT_SIZE = 128;
     private static final int DEFAULT_NUM_ITERATIONS = 1;
-    private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_ITERATIONS;
+    private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = Argon2HashProvider.Parameters.DEFAULT_ITERATIONS;
 
-    private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name.  Defaults to SHA-256 when password hashing, MD5 otherwise.");
+    private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name.  Defaults to Argon2 when password hashing, SHA-512 otherwise.");
     private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information.");
-    private static final Option FORMAT = new Option("f", "format", true, "hash output format.  Defaults to 'shiro1' when password hashing, 'hex' otherwise.  See below for more information.");
+    private static final Option FORMAT = new Option("f", "format", true, "hash output format. Defaults to 'shiro2' when password hashing, 'hex' otherwise.  See below for more information.");
     private static final Option HELP = new Option("help", "help", false, "show this help message.");
     private static final Option ITERATIONS = new Option("i", "iterations", true, "number of hash iterations.  Defaults to " + DEFAULT_PASSWORD_NUM_ITERATIONS + " when password hashing, 1 otherwise.");
     private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)");
@@ -223,18 +232,17 @@ public final class Hasher {
             }
 
             ByteSource publicSalt = getSalt(saltString, saltBytesString, generateSalt, generatedSaltSize);
-            ByteSource privateSalt = getSalt(privateSaltString, privateSaltBytesString, false, generatedSaltSize);
-            HashRequest hashRequest = new SimpleHashRequest(algorithm, ByteSource.Util.bytes(source), publicSalt, iterations);
+            // FIXME: add options here.
+            HashRequest hashRequest = new SimpleHashRequest(algorithm, ByteSource.Util.bytes(source), publicSalt, emptyMap());
 
             DefaultHashService hashService = new DefaultHashService();
-            hashService.setPrivateSalt(privateSalt);
             Hash hash = hashService.computeHash(hashRequest);
 
             if (formatString == null) {
-                //Output format was not specified.  Default to 'shiro1' when password hashing, and 'hex' for
+                //Output format was not specified.  Default to 'shiro2' when password hashing, and 'hex' for
                 //everything else:
                 if (password) {
-                    formatString = Shiro1CryptFormat.class.getName();
+                    formatString = Shiro2CryptFormat.class.getName();
                 } else {
                     formatString = HexFormat.class.getName();
                 }
@@ -248,7 +256,7 @@ public final class Hasher {
 
             String output = format.format(hash);
 
-            System.out.println(output);
+            LOG.info(output);
 
         } catch (IllegalArgumentException iae) {
             exit(iae, debug);
@@ -339,16 +347,16 @@ public final class Hasher {
 
     private static void printException(Exception e, boolean debug) {
         if (e != null) {
-            System.out.println();
+            LOG.info("");
             if (debug) {
-                System.out.println("Error: ");
+                LOG.info("Error: ");
                 e.printStackTrace(System.out);
-                System.out.println(e.getMessage());
+                LOG.info(e.getMessage());
 
             } else {
-                System.out.println("Error: " + e.getMessage());
-                System.out.println();
-                System.out.println("Specify -d or --debug for more information.");
+                LOG.info("Error: " + e.getMessage());
+                LOG.info("");
+                LOG.info("Specify -d or --debug for more information.");
             }
         }
     }
@@ -388,7 +396,7 @@ public final class Hasher {
                 "a positive integer (size is in bits, not bytes)." +
                 "\n\n" +
                 "Because a salt must be specified if computing the hash later,\n" +
-                "generated salts are only useful with the shiro1 output format;\n" +
+                "generated salts are only useful with the shiro1/shiro2 output format;\n" +
                 "the other formats do not include the generated salt." +
                 "\n\n" +
                 "Specifying a private salt:" +
@@ -424,16 +432,16 @@ public final class Hasher {
                 "by the " + DefaultHashFormatFactory.class.getName() + "\n" +
                 "JavaDoc) or 2) the fully qualified " + HashFormat.class.getName() + "\n" +
                 "implementation class name to instantiate and use for formatting.\n\n" +
-                "The default output format is 'shiro1' which is a Modular Crypt Format (MCF)\n" +
+                "The default output format is 'shiro2' which is a Modular Crypt Format (MCF)\n" +
                 "that shows all relevant information as a dollar-sign ($) delimited string.\n" +
                 "This format is ideal for use in Shiro's text-based user configuration (e.g.\n" +
                 "shiro.ini or a properties file).";
 
         printException(e, debug);
 
-        System.out.println();
+        LOG.info("");
         help.printHelp(command, header, options, null);
-        System.out.println(footer);
+        LOG.info(footer);
     }
 
     private static void printHelpAndExit(Options options, Exception e, boolean debug, int exitCode) {
@@ -441,12 +449,20 @@ public final class Hasher {
         System.exit(exitCode);
     }
 
-    private static char[] readPassword(boolean confirm) {
+    private static char[] readPassword(boolean confirm) throws IOException {
         java.io.Console console = System.console();
-        if (console == null) {
-            throw new IllegalStateException("java.io.Console is not available on the current JVM.  Cannot read passwords.");
+        char[] first;
+        if (console != null) {
+            first = console.readPassword("%s", "Password to hash: ");
+            //throw new IllegalStateException("java.io.Console is not available on the current JVM.  Cannot read passwords.");
+        } else if (System.in != null) {
+            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+            String readLine = br.readLine();
+            first = readLine.toCharArray();
+        } else {
+            throw new IllegalStateException("java.io.Console and java.lang.System.in are not available on the current JVM. Cannot read passwords.");
         }
-        char[] first = console.readPassword("%s", "Password to hash: ");
+
         if (first == null || first.length == 0) {
             throw new IllegalArgumentException("No password specified.");
         }
diff --git a/crypto/pom.xml b/tools/hasher/src/main/resources/logback.xml
similarity index 52%
copy from crypto/pom.xml
copy to tools/hasher/src/main/resources/logback.xml
index b7f0e68..502d9d1 100644
--- a/crypto/pom.xml
+++ b/tools/hasher/src/main/resources/logback.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one
   ~ or more contributor license agreements.  See the NOTICE file
@@ -17,26 +16,16 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
 
-    <modelVersion>4.0.0</modelVersion>
+<configuration>
 
-    <parent>
-        <groupId>org.apache.shiro</groupId>
-        <artifactId>shiro-root</artifactId>
-        <version>2.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
-    </parent>
-
-    <artifactId>shiro-crypto</artifactId>
-    <name>Apache Shiro :: Cryptography</name>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>core</module>
-        <module>hash</module>
-        <module>cipher</module>
-    </modules>
-
-</project>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%-5level] %msg%n</pattern>
+        </encoder>
+    </appender>
 
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java
new file mode 100644
index 0000000..00e6286
--- /dev/null
+++ b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+package org.apache.shiro.tools.hasher;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @since 2.0
+ */
+public class HasherTest {
+
+    private final InputStream systemIn = System.in;
+
+    private ByteArrayInputStream testIn;
+
+    private final Logger hasherToolLogger = (Logger) LoggerFactory.getLogger("ROOT");
+    private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
+
+    @BeforeEach
+    public void setUpOutput() {
+        hasherToolLogger.detachAndStopAllAppenders();
+        hasherToolLogger.addAppender(listAppender);
+        listAppender.start();
+    }
+
+    private void provideInput(String data) {
+        testIn = new ByteArrayInputStream(data.getBytes());
+        System.setIn(testIn);
+    }
+
+    @AfterEach
+    public void restoreSystemInputOutput() throws IOException {
+        System.setIn(systemIn);
+        testIn.close();
+        listAppender.stop();
+    }
+
+
+    @Test
+    public void testArgon2Hash() {
+        // given
+        String[] args = {"--debug", "--password", "--pnoconfirm"};
+        provideInput("secret#shiro,password;Jo8opech");
+
+        // when
+        Hasher.main(args);
+        List<ILoggingEvent> loggingEvents = listAppender.list;
+
+        // when
+        assertEquals(1, loggingEvents.size());
+        ILoggingEvent iLoggingEvent = loggingEvents.get(0);
+        assertTrue(iLoggingEvent.getMessage().contains("$shiro2$argon2id$v=19"));
+    }
+
+    @Test
+    public void testBCryptHash() {
+        // given
+        String[] args = {"--debug", "--password", "--pnoconfirm", "--algorithm", "2y"};
+        provideInput("secret#shiro,password;Jo8opech");
+
+        // when
+        Hasher.main(args);
+        List<ILoggingEvent> loggingEvents = listAppender.list;
+
+        // when
+        assertEquals(1, loggingEvents.size());
+        ILoggingEvent iLoggingEvent = loggingEvents.get(0);
+        assertTrue(iLoggingEvent.getMessage().contains("$shiro2$2y$10$"));
+    }
+}
diff --git a/crypto/pom.xml b/tools/hasher/src/test/resources/logback-test.xml
similarity index 52%
copy from crypto/pom.xml
copy to tools/hasher/src/test/resources/logback-test.xml
index b7f0e68..a652392 100644
--- a/crypto/pom.xml
+++ b/tools/hasher/src/test/resources/logback-test.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one
   ~ or more contributor license agreements.  See the NOTICE file
@@ -17,26 +16,13 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
 
-    <modelVersion>4.0.0</modelVersion>
+<configuration>
 
-    <parent>
-        <groupId>org.apache.shiro</groupId>
-        <artifactId>shiro-root</artifactId>
-        <version>2.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
-    </parent>
-
-    <artifactId>shiro-crypto</artifactId>
-    <name>Apache Shiro :: Cryptography</name>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>core</module>
-        <module>hash</module>
-        <module>cipher</module>
-    </modules>
-
-</project>
+    <appender name="list" class="ch.qos.logback.core.read.ListAppender">
+    </appender>
 
+    <root level="info">
+        <appender-ref ref="list"/>
+    </root>
+</configuration>