Merge pull request #302 from weltonrodrigo/patch-1

[SHIRO-817] Update `CommonsInterpolator` doc to reflect actual behavior
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index 2bac2df..44d7168 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.7.1

 ###########################################################

 

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 @@
  */
 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 @@
      *
      * @since 1.1
      */
-    protected ByteSource credentialsSalt;
+    protected ByteSource credentialsSalt = SimpleByteSource.empty();
 
     /**
      * Default no-argument constructor.
@@ -124,6 +127,7 @@
     }
 
 
+    @Override
     public PrincipalCollection getPrincipals() {
         return principals;
     }
@@ -137,6 +141,7 @@
         this.principals = principals;
     }
 
+    @Override
     public Object getCredentials() {
         return credentials;
     }
@@ -163,6 +168,7 @@
      *         hashed at all.
      * @since 1.1
      */
+    @Override
     public ByteSource getCredentialsSalt() {
         return credentialsSalt;
     }
@@ -189,6 +195,7 @@
      *
      * @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 @@
      * @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 @@
      *
      * @return the hashcode of the internal {@link #getPrincipals() principals} instance.
      */
+    @Override
     public int hashCode() {
         return (principals != null ? principals.hashCode() : 0);
     }
@@ -275,6 +290,7 @@
      *
      * @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 @@
 
     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 @@
         return hashService.computeHash(request);
     }
 
+    @Override
     public boolean passwordsMatch(Object plaintext, Hash saved) {
         ByteSource plaintextBytes = createByteSource(plaintext);
 
@@ -92,11 +105,7 @@
             }
         }
 
-        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 @@
         return ByteSource.Util.bytes(o);
     }
 
+    @Override
     public boolean passwordsMatch(Object submittedPlaintext, String saved) {
         ByteSource plaintextBytes = createByteSource(submittedPlaintext);
 
@@ -151,9 +161,9 @@
         //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 @@
         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 @@
 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 @@
  * 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 @@
  * 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 @@
      * @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 @@
      * @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 @@
      * 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 @@
 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 @@
         this.passwordService = new DefaultPasswordService();
     }
 
+    @Override
     public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
 
         PasswordService service = ensurePasswordService();
@@ -49,23 +51,11 @@
 
         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 a33fbdd..8feb159 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 @@
 
             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..38ad06d 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
@@ -19,14 +19,19 @@
 package org.apache.shiro.authc.credential
 
 import org.apache.shiro.crypto.SecureRandomNumberGenerator
-import org.apache.shiro.crypto.hash.*
+import org.apache.shiro.crypto.hash.DefaultHashService
+import org.apache.shiro.crypto.hash.Hash
+import org.apache.shiro.crypto.hash.Sha384Hash
+import org.apache.shiro.crypto.hash.Sha512Hash
 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,52 +41,22 @@
 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 encrypted = service.encryptPassword("12345")
-        assertTrue service.passwordsMatch("12345", encrypted)
-    }
-
-    @Test
-    void testEncryptPasswordWithInvalidMatch() {
-        def service = new DefaultPasswordService()
-        def encrypted = service.encryptPassword("ABCDEF")
-        assertFalse service.passwordsMatch("ABC", encrypted)
-    }
-
-    @Test
-    void testBackwardsCompatibility() {
-        def service = new DefaultPasswordService()
-        def encrypted = service.encryptPassword("12345")
-        def submitted = "12345"
-        assertTrue service.passwordsMatch(submitted, encrypted);
-
-        //change some settings:
-        service.hashService.hashAlgorithmName = "MD5"
-        service.hashService.hashIterations = 250000
-
-        def encrypted2 = service.encryptPassword(submitted)
-
-        assertFalse encrypted == encrypted2
-
-        assertTrue service.passwordsMatch(submitted, encrypted2)
-    }
-
-    @Test
     void testHashFormatWarned() {
-        def service = new DefaultPasswordService()
+        def service = createSha256Service()
         service.hashFormat = new HexFormat()
         assertTrue service.hashFormat instanceof HexFormat
         service.encryptPassword("test")
@@ -90,33 +65,13 @@
 
     @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"))
-    }
-
-    @Test
-    void testCustomHashService() {
-        def hashService = createMock(HashService)
-
-        def hash = new Sha256Hash("test", new SecureRandomNumberGenerator().nextBytes(), 100);
-
-        expect(hashService.computeHash(isA(HashRequest))).andReturn hash
-
-        replay hashService
-
-        def service = new DefaultPasswordService()
-        service.hashService = hashService
-
-        def returnedHash = service.encryptPassword("test")
-
-        assertEquals new Shiro1CryptFormat().format(hash), returnedHash
-
-        verify hashService
+        assertFalse service.passwordsMatch(null, new Sha384Hash("test"))
     }
 
     @Test
@@ -140,35 +95,8 @@
         verify factory
     }
 
-    @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();
-
-        // tr_TR
-        Locale.setDefault(new Locale("tr", "TR"));
-
-        try {
-            PasswordService passwordService = new DefaultPasswordService();
-            String password = "333";
-            String enc = passwordService.encryptPassword(password);
-            assertTrue(passwordService.passwordsMatch(password, enc));
-        }
-        finally {
-            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 @@
 
 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 @@
         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 @@
         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 @@
         }
 
         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.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 @@
     @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 @@
      */
     @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 @@
      */
     @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 @@
         //(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 @@
 @Deprecated
 public abstract class AbstractHash extends CodecSupport implements Hash, Serializable {
 
+    private static final long serialVersionUID = -4723044219611288405L;
     /**
      * The hashed data
      */
@@ -142,8 +143,10 @@
      *
      * @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 @@
      *
      * @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 @@
      *
      * @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 @@
      *
      * @return the {@link #toHex() toHex()} value.
      */
+    @Override
     public String toString() {
         return toHex();
     }
@@ -274,6 +280,7 @@
      * @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 @@
      *
      * @return toHex().hashCode()
      */
+    @Override
     public int hashCode() {
         if (this.bytes == null || this.bytes.length == 0) {
             return 0;
@@ -294,68 +302,4 @@
         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 @@
 public interface ConfigurableHashService extends HashService {
 
     /**
-     * Sets the 'private' (internal) salt to be paired with a 'public' (random or supplied) salt during hash computation.
+     * Sets the name of the key derivation function algorithm that will be used to compute
+     * secure hashes for passwords.
      *
-     * @param privateSalt the 'private' internal salt to be paired with a 'public' (random or supplied) salt during
-     *                    hash computation.
+     * @param name the name of the key derivation function algorithm that will be used to
+     *             compute secure hashes for passwords.
      */
-    void setPrivateSalt(ByteSource privateSalt);
+    void setDefaultAlgorithmName(String name);
 
-    /**
-     * 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.
-     *
-     * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to
-     *             compute hashes.
-     */
-    void setHashAlgorithmName(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 @@
  * 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;
+    private String defaultAlgorithmName;
 
-    /**
-     * The 'private' part of the hash salt.
-     */
-    private ByteSource privateSalt;
-
-    /**
-     * The number of hash iterations to perform when computing hashes.
-     */
-    private int iterations;
-
-    /**
-     * 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 @@
      * <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);
+        Optional<HashSpi> kdfHash = HashProvider.getByAlgorithmName(algorithmName);
+        if (kdfHash.isPresent()) {
+            HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new);
 
-        Hash computed = new SimpleHash(algorithmName, source, salt, iterations);
+            return hashSpi.newHashFactory(random).generate(request);
+        }
 
-        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;
+        throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName);
     }
 
+
     protected String getAlgorithmName(HashRequest request) {
-        String name = request.getAlgorithmName();
-        if (name == null) {
-            name = getHashAlgorithmName();
-        }
-        return name;
+        return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName);
     }
 
-    protected int getIterations(HashRequest request) {
-        int iterations = Math.max(0, request.getIterations());
-        if (iterations < 1) {
-            iterations = Math.max(1, getHashIterations());
-        }
-        return iterations;
+    @Override
+    public void setDefaultAlgorithmName(String name) {
+        this.defaultAlgorithmName = name;
     }
 
-    /**
-     * 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;
+    public String getDefaultAlgorithmName() {
+        return this.defaultAlgorithmName;
     }
 
-    /**
-     * 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;
-    }
-
-    public void setRandomNumberGenerator(RandomNumberGenerator rng) {
-        this.rng = rng;
-    }
-
-    public RandomNumberGenerator getRandomNumberGenerator() {
-        return this.rng;
-    }
-
-    /**
-     * 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 @@
  * 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 @@
      */
     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 @@
      * @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 @@
      * 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.
      */
-    String getAlgorithmName();
+    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}.
+     */
+    Map<String, Object> getParameters();
 
     /**
      * A Builder class representing the Builder design pattern for constructing {@link HashRequest} instances.
@@ -85,15 +97,14 @@
     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 @@
             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 @@
          * @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 @@
 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 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 @@
     }

 

     /**

+     * 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 @@
         }

         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 @@
     /**

      * 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 @@
      *

      * @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 @@
      *

      * @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 @@
      * @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 @@
      * @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 @@
      * @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 @@
      * @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 @@
         return hashed;

     }

 

+    @Override

     public boolean isEmpty() {

         return this.bytes == null || this.bytes.length == 0;

     }

@@ -368,6 +414,7 @@
      *

      * @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 @@
      *

      * @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 @@
      *

      * @return the {@link #toHex() toHex()} value.

      */

+    @Override

     public String toString() {

         return toHex();

     }

@@ -409,6 +458,7 @@
      * @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 @@
      *

      * @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 @@
 
 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 @@
 
     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 @@
 
 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 @@
         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 @@
             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 @@
  * 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 @@
      *
      * @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 @@
  * 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 @@
     /**
      * 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 @@
         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 @@
             // 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 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 @@
         return sb.toString();
     }
 
-    public Hash parse(String formatted) {
+    @Override
+    public Hash parse(final String formatted) {
         if (formatted == null) {
             return null;
         }
@@ -130,13 +134,17 @@
         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..f781c2b
--- /dev/null
+++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java
@@ -0,0 +1,137 @@
+/*
+ * 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 static java.util.Objects.requireNonNull;
+
+/**
+ * The {@code Shiro2CryptFormat} 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)");
+
+        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
+        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 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 @@
 
     @Test
     void testNullRequest() {
-        assertNull createService().computeHash(null)
+        assertNull createSha256Service().computeHash(null)
     }
 
     @Test
     void testDifferentAlgorithmName() {
-        def service = new DefaultHashService(hashAlgorithmName: 'MD5')
+        // given
+        def newAlgorithm = 'SHA-512'
+        def service = new DefaultHashService(defaultAlgorithmName: newAlgorithm)
+
+        // when
         def hash = hash(service, "test")
-        assertEquals 'MD5', hash.algorithmName
-    }
 
-    @Test
-    void testDifferentIterations() {
-        def service = new DefaultHashService(hashIterations: 2)
-        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 @@
      */
     @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 @@
      */
     @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 @@
      */
     @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 @@
      */
     @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 @@
         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 static DefaultHashService createSha256Service() {
+        return new DefaultHashService(defaultAlgorithmName: 'SHA-256');
     }
 
-    private DefaultHashService createService() {
-        return new DefaultHashService();
-    }
-
-    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 @@
 
 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.Assert.*
+import static org.junit.jupiter.api.Assertions.*
+
 
 /**
  * Unit tests for the {@link HashRequest.Builder} implementation
@@ -33,16 +34,7 @@
 
     @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 @@
         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 @@
 
     @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 @@
     @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 @@
     @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 @@
     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 @@
 
     @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 @@
     @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 @@
     @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 @@
 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 @@
         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 @@
         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 @@
         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/support/hashes/argon2/pom.xml b/crypto/support/hashes/argon2/pom.xml
new file mode 100644
index 0000000..b278be5
--- /dev/null
+++ b/crypto/support/hashes/argon2/pom.xml
@@ -0,0 +1,79 @@
+<?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
+  ~ 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.
+  -->
+
+<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.crypto</groupId>
+        <artifactId>shiro-crypto-support</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>shiro-hashes-argon2</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support :: Argon2</name>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shiro</groupId>
+            <artifactId>shiro-crypto-hash</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <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*;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>
+
+</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..86bcdca
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java
@@ -0,0 +1,371 @@
+/*
+ * 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>
+ *
+ * <p>Default values are taken from <a href="https://datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/?include_text=1">draft-irtf-cfrg-argon2-13</a>.
+ * This implementation is using the parameters from section 4, paragraph 2 (memory constrained environment).</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;
+
+    /**
+     * Number of iterations, default taken from draft-irtf-cfrg-argon2-13, 4.2.
+     */
+    public static final int DEFAULT_ITERATIONS = 1;
+
+    /**
+     * Amount of memory, default (64 MiB) taken from draft-irtf-cfrg-argon2-13, 4.2.
+     */
+    public static final int DEFAULT_MEMORY_KIB = 64 * 1024;
+
+    private static final Set<String> ALGORITHMS_ARGON2 = new HashSet<>(Arrays.asList("argon2id", "argon2i", "argon2d"));
+
+    private static final Pattern DELIMITER_COMMA = Pattern.compile(",");
+
+    /**
+     * Number of default lanes, p=4 is the default recommendation, taken from draft-irtf-cfrg-argon2-13, 4.2.
+     */
+    public static final int DEFAULT_PARALLELISM = 4;
+
+    /**
+     * 256 bits tag size is the default recommendation, taken from draft-irtf-cfrg-argon2-13, 4.2.
+     */
+    public static final int DEFAULT_OUTPUT_LENGTH_BITS = 256;
+
+
+    /**
+     * 128 bits of salt is the recommended salt length, taken from draft-irtf-cfrg-argon2-13, 4.2.
+     */
+    private static final int SALT_LENGTH_BITS = 128;
+
+    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_BITS / 8));
+    }
+
+    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_BITS);
+    }
+
+    public static Argon2Hash generate(
+            String algorithmName,
+            int argonVersion,
+            ByteSource source,
+            ByteSource salt,
+            int iterations,
+            int memoryAsKB,
+            int parallelism,
+            int outputLengthBits
+    ) {
+        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[outputLengthBits / 8];
+        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 * 8);
+
+            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_BITS / 8;
+    }
+
+    @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..2a8fdee
--- /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 outputLengthBits = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_OUTPUT_LENGTH_BITS))
+                    .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_OUTPUT_LENGTH_BITS))
+                    .orElse(Parameters.DEFAULT_OUTPUT_LENGTH_BITS);
+
+            return Argon2Hash.generate(
+                    algorithmName,
+                    version,
+                    hashRequest.getSource(),
+                    salt,
+                    iterations,
+                    memoryKib,
+                    parallelism,
+                    outputLengthBits
+            );
+        }
+
+        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_BITS = Argon2Hash.DEFAULT_OUTPUT_LENGTH_BITS;
+
+        /**
+         * 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 (in bits) 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_BITS} when this parameter is not specified.</p>
+         */
+        public static final String PARAMETER_OUTPUT_LENGTH_BITS = "Argon2.outputLength";
+
+        private Parameters() {
+            // utility class
+        }
+    }
+}
diff --git a/samples/jaxrs/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
similarity index 78%
rename from samples/jaxrs/src/main/resources/META-INF/NOTICE
rename to crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
index 9d26a95..4b3b138 100644
--- a/samples/jaxrs/src/main/resources/META-INF/NOTICE
+++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
@@ -4,10 +4,13 @@
 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..7ba7ff3
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy
@@ -0,0 +1,92 @@
+/*
+ * 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'
+
+        // when
+        def hash = new Shiro2CryptFormat().parse(shiro2Format) as Argon2Hash;
+        def matchesPassword = hash.matchesPassword TEST_PASSWORD_BS;
+
+        // then
+        assertEquals Argon2Parameters.ARGON2_VERSION_13, hash.argonVersion
+        assertEquals 3, hash.iterations
+        assertEquals 4096, hash.memoryKiB
+        assertEquals 4, hash.parallelism
+        assertTrue matchesPassword
+    }
+
+    /**
+     * Modern formats do not fit well / at all into the existing shiro1 crypt format.
+     *
+     * <p>This test just makes sure trying to squeeze Argon2 into a Shiro1CryptFormat string will throw
+     * the correct exception every time.</p>
+     */
+    @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/support/hashes/bcrypt/pom.xml b/crypto/support/hashes/bcrypt/pom.xml
new file mode 100644
index 0000000..ae9022c
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/pom.xml
@@ -0,0 +1,79 @@
+<?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
+  ~ 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.
+  -->
+
+<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.crypto</groupId>
+        <artifactId>shiro-crypto-support</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>shiro-hashes-bcrypt</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support :: BCrypt</name>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shiro</groupId>
+            <artifactId>shiro-crypto-hash</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <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*;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>
+
+</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/samples/jaxrs/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
similarity index 78%
copy from samples/jaxrs/src/main/resources/META-INF/NOTICE
copy to crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
index 9d26a95..4b3b138 100644
--- a/samples/jaxrs/src/main/resources/META-INF/NOTICE
+++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
@@ -4,10 +4,13 @@
 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/support/pom.xml b/crypto/support/pom.xml
new file mode 100644
index 0000000..582fe24
--- /dev/null
+++ b/crypto/support/pom.xml
@@ -0,0 +1,44 @@
+<?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
+  ~ 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.
+  -->
+
+<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>
+        <version>2.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.shiro.crypto</groupId>
+    <artifactId>shiro-crypto-support</artifactId>
+    <name>Apache Shiro :: Cryptography :: Support</name>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>hashes/argon2</module>
+        <module>hashes/bcrypt</module>
+    </modules>
+
+
+</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 @@
 
 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 @@
      * 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 @@
                 o instanceof ByteSource || o instanceof File || o instanceof InputStream;
     }
 
-    public byte[] getBytes() {
-        return this.bytes;
+    public static ByteSource empty() {
+        return new SimpleByteSource(new byte[]{});
     }
 
+    @Override
+    public byte[] getBytes() {
+        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 @@
         return this.cachedHex;
     }
 
+    @Override
     public String toBase64() {
         if ( this.cachedBase64 == null ) {
             this.cachedBase64 = Base64.encodeToString(getBytes());
@@ -152,10 +160,12 @@
         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 @@
         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 @@
         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 91be17f..6246a27 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>
@@ -399,9 +400,9 @@
                     <version>3.0.0-M2</version>
                 </plugin>
                 <plugin>
-                    <groupId>com.nickwongdev</groupId>
+                    <groupId>dev.aspectj</groupId>
                     <artifactId>aspectj-maven-plugin</artifactId>
-                    <version>1.12.6</version>
+                    <version>1.13.M2</version>
                     <!-- Using a fork, until such time that the aspect-maven-plugin updates to support JDK 11 - https://github.com/mojohaus/aspectj-maven-plugin/pull/45
                     <groupId>org.codehaus.mojo</groupId>
                     <artifactId>aspectj-maven-plugin</artifactId>
@@ -752,6 +753,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>
@@ -1196,6 +1207,11 @@
                 <artifactId>spring-boot-test</artifactId>
                 <version>${spring-boot.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-web</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
 
             <!-- Guice -->
             <dependency>
@@ -1224,6 +1240,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/samples/aspectj/pom.xml b/samples/aspectj/pom.xml
index 748cb0b..f3a5cfb 100644
--- a/samples/aspectj/pom.xml
+++ b/samples/aspectj/pom.xml
@@ -34,7 +34,7 @@
 	<build>
 		<plugins>
 			<plugin>
-				<groupId>com.nickwongdev</groupId>
+				<groupId>dev.aspectj</groupId>
 				<artifactId>aspectj-maven-plugin</artifactId>
 				<!-- Using a fork, until such time that the aspect-maven-plugin updates to support JDK 11 - https://github.com/mojohaus/aspectj-maven-plugin/pull/45
 				<groupId>org.codehaus.mojo</groupId>
diff --git a/samples/jaxrs/pom.xml b/samples/jaxrs/pom.xml
deleted file mode 100644
index 6878195..0000000
--- a/samples/jaxrs/pom.xml
+++ /dev/null
@@ -1,221 +0,0 @@
-<?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
-  ~ 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.
-  -->
-<!--suppress osmorcNonOsgiMavenDependency -->
-<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">
-
-    <parent>
-        <groupId>org.apache.shiro.samples</groupId>
-        <artifactId>shiro-samples</artifactId>
-        <version>2.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
-    </parent>
-
-    <modelVersion>4.0.0</modelVersion>
-    <artifactId>samples-jaxrs</artifactId>
-    <name>Apache Shiro :: Samples :: JAX-RS</name>
-    <packaging>war</packaging>
-
-    <build>
-        <pluginManagement>
-            <plugins>
-                <plugin>
-                    <groupId>org.eclipse.jetty</groupId>
-                    <artifactId>jetty-maven-plugin</artifactId>
-                    <version>${jetty.version}</version>
-                    <configuration>
-                        <contextPath>/</contextPath>
-                        <httpConnector>
-                            <port>9080</port>
-                            <idleTimeout>60000</idleTimeout>
-                        </httpConnector>
-                        <requestLog implementation="org.eclipse.jetty.server.NCSARequestLog">
-                            <filename>./target/yyyy_mm_dd.request.log</filename>
-                            <retainDays>90</retainDays>
-                            <append>true</append>
-                            <extended>false</extended>
-                            <logTimeZone>GMT</logTimeZone>
-                        </requestLog>
-                    </configuration>
-                </plugin>
-            </plugins>
-        </pluginManagement>
-        <plugins>
-            <plugin>
-                <groupId>org.eclipse.jetty</groupId>
-                <artifactId>jetty-maven-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
-    <dependencies>
-
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-servlet-plugin</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-jaxrs</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>jakarta.ws.rs</groupId>
-            <artifactId>jakarta.ws.rs-api</artifactId>
-            <version>${jaxrs.api.version}</version>
-            <scope>provided</scope>
-        </dependency>
-
-        <dependency>
-            <!-- Required for any libraries that expect to call the commons logging APIs -->
-            <groupId>org.slf4j</groupId>
-            <artifactId>jcl-over-slf4j</artifactId>
-            <scope>runtime</scope>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-classic</artifactId>
-            <scope>runtime</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.shiro.integrationtests</groupId>
-            <artifactId>shiro-its-support</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>com.jayway.restassured</groupId>
-            <artifactId>rest-assured</artifactId>
-            <version>2.9.0</version>
-            <scope>test</scope>
-            <exclusions>
-                <exclusion>
-                    <groupId>commons-logging</groupId>
-                    <artifactId>commons-logging</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-
-    </dependencies>
-
-    <profiles>
-        <profile>
-            <id>jersey</id>
-            <activation>
-                <activeByDefault>true</activeByDefault>
-            </activation>
-            <properties>
-                <jersey.version>2.23.2</jersey.version>
-            </properties>
-            <dependencies>
-                <dependency>
-                    <groupId>org.glassfish.jersey.containers</groupId>
-                    <artifactId>jersey-container-grizzly2-servlet</artifactId>
-                    <version>${jersey.version}</version>
-                </dependency>
-            </dependencies>
-        </profile>
-        <profile>
-            <id>resteasy</id>
-            <properties>
-                <resteasy.version>3.9.0.Final</resteasy.version>
-            </properties>
-            <dependencies>
-                <dependency>
-                    <groupId>org.jboss.resteasy</groupId>
-                    <artifactId>resteasy-jaxrs</artifactId>
-                    <version>${resteasy.version}</version>
-                </dependency>
-
-                <dependency>
-                    <groupId>org.jboss.resteasy</groupId>
-                    <artifactId>resteasy-servlet-initializer</artifactId>
-                    <version>${resteasy.version}</version>
-                </dependency>
-
-                <dependency>
-                    <groupId>org.jboss.resteasy</groupId>
-                    <artifactId>resteasy-jackson2-provider</artifactId>
-                    <version>${resteasy.version}</version>
-                </dependency>
-            </dependencies>
-        </profile>
-        <profile>
-            <id>cxf</id>
-            <properties>
-                <cxf.version>3.3.5</cxf.version>
-            </properties>
-            <dependencies>
-                <dependency>
-                    <groupId>org.apache.cxf</groupId>
-                    <artifactId>cxf-rt-rs-http-sci</artifactId>
-                    <version>${cxf.version}</version>
-                </dependency>
-                <dependency>
-                    <groupId>org.apache.cxf</groupId>
-                    <artifactId>cxf-rt-frontend-jaxws</artifactId>
-                    <version>${cxf.version}</version>
-                </dependency>
-            </dependencies>
-            <build>
-                <plugins>
-                    <plugin>
-                        <groupId>org.eclipse.jetty</groupId>
-                        <artifactId>jetty-maven-plugin</artifactId>
-                        <configuration>
-                            <webApp>
-                                <descriptor>src/main/webapp/WEB-INF/web.cxf.xml</descriptor>
-                            </webApp>
-                        </configuration>
-                    </plugin>
-                    <plugin>
-                        <artifactId>maven-war-plugin</artifactId>
-                        <configuration>
-                            <webXml>src/main/webapp/WEB-INF/web.cxf.xml</webXml>
-                        </configuration>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-        <!-- Currently the test fails with JDK11, so exclude it -->
-        <profile>
-            <id>jdk19-plus</id>
-            <activation>
-                <jdk>[9,)</jdk>
-            </activation>
-            <build>
-                <plugins>
-                   <plugin>
-                       <groupId>org.apache.maven.plugins</groupId>
-                       <artifactId>maven-failsafe-plugin</artifactId>
-                       <configuration>
-                           <excludes>
-                               <exclude>**/ContainerIntegrationIT.*</exclude>
-                           </excludes>
-                       </configuration>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-
-    </profiles>
-
-</project>
diff --git a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/SampleApplication.java b/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/SampleApplication.java
deleted file mode 100644
index b7ae949..0000000
--- a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/SampleApplication.java
+++ /dev/null
@@ -1,50 +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.samples.jaxrs;
-
-import org.apache.shiro.samples.jaxrs.resources.HelloResource;
-import org.apache.shiro.samples.jaxrs.resources.SecureResource;
-import org.apache.shiro.web.jaxrs.ShiroFeature;
-
-import javax.ws.rs.ApplicationPath;
-import javax.ws.rs.core.Application;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Simple JAX-RS {@link Application} that is implementation agnostic.
- * @since 1.4
- */
-@ApplicationPath("/")
-public class SampleApplication extends Application {
-
-    @Override
-    public Set<Class<?>> getClasses() {
-        Set<Class<?>> classes = new HashSet<Class<?>>();
-
-        // register Shiro
-        classes.add(ShiroFeature.class);
-
-        // register resources
-        classes.add(HelloResource.class);
-        classes.add(SecureResource.class);
-
-        return classes;
-    }
-}
diff --git a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/HelloResource.java b/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/HelloResource.java
deleted file mode 100644
index 6ba4d64..0000000
--- a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/HelloResource.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.samples.jaxrs.resources;
-
-
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.Suspended;
-
-@Path("say")
-public class HelloResource {
-
-
-    @Produces({"application/json","plain/text"})
-    @GET
-    public String saySomething(@QueryParam("words") @DefaultValue("Hello!") String words) {
-        return words;
-    }
-
-    @Produces({"application/json","plain/text"})
-    @GET
-    @Path("async")
-    public void saySomethingAsync(@QueryParam("words") @DefaultValue("Hello!") String words,
-                                    @Suspended AsyncResponse asyncResponse) {
-        asyncResponse.resume(words);
-    }
-}
diff --git a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/SecureResource.java b/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/SecureResource.java
deleted file mode 100644
index c590987..0000000
--- a/samples/jaxrs/src/main/java/org/apache/shiro/samples/jaxrs/resources/SecureResource.java
+++ /dev/null
@@ -1,75 +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.samples.jaxrs.resources;
-
-
-import org.apache.shiro.authz.annotation.RequiresAuthentication;
-import org.apache.shiro.authz.annotation.RequiresGuest;
-import org.apache.shiro.authz.annotation.RequiresPermissions;
-import org.apache.shiro.authz.annotation.RequiresRoles;
-import org.apache.shiro.authz.annotation.RequiresUser;
-
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-
-@Path("secure")
-@Produces({"application/json","plain/text"})
-public class SecureResource {
-
-
-    @RequiresPermissions("lightsaber:requiresPermissions")
-    @Path("RequiresPermissions")
-    @GET
-    public String protectedByRequiresPermissions() {
-        return "protected";
-    }
-
-    @RequiresRoles("admin")
-    @Path("RequiresRoles")
-    @GET
-    public String protectedByRequiresRoles() {
-        return "protected";
-    }
-
-    @RequiresUser
-    @Path("RequiresUser")
-    @GET
-    public String protectedByRequiresUser() {
-        return "protected";
-    }
-
-    @RequiresGuest
-    @Path("RequiresGuest")
-    @GET
-    public String protectedByRequiresGuest() {
-        return "not protected";
-    }
-
-    @RequiresAuthentication
-    @Path("RequiresAuthentication")
-    @GET
-    public String protectedByRequiresAuthentication() {
-        return "protected";
-    }
-
-
-}
diff --git a/samples/jaxrs/src/main/resources/shiro.ini b/samples/jaxrs/src/main/resources/shiro.ini
deleted file mode 100644
index 54fa949..0000000
--- a/samples/jaxrs/src/main/resources/shiro.ini
+++ /dev/null
@@ -1,43 +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.
-
-[main]
-cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
-
-sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
-sessionManager.sessionIdUrlRewritingEnabled = false
-
-securityManager.sessionManager = $sessionManager
-securityManager.cacheManager = $cacheManager
-
-[urls]
-/** = authcBasic[permissive]
-
-[users]
-# format: username = password, role1, role2, ..., roleN
-root = secret,admin
-guest = guest,guest
-presidentskroob = 12345,president
-darkhelmet = ludicrousspeed,darklord,schwartz
-lonestarr = vespa,goodguy,schwartz
-
-[roles]
-# format: roleName = permission1, permission2, ..., permissionN
-admin = *
-schwartz = lightsaber:*
-goodguy = winnebago:drive:eagle5
diff --git a/samples/jaxrs/src/main/webapp/WEB-INF/web.cxf.xml b/samples/jaxrs/src/main/webapp/WEB-INF/web.cxf.xml
deleted file mode 100644
index 3e5fbe9..0000000
--- a/samples/jaxrs/src/main/webapp/WEB-INF/web.cxf.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?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
-  ~ 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.
-  -->
-
-<web-app version="3.1"
-         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
-
-    <servlet>
-        <servlet-name>CXFServlet</servlet-name>
-        <display-name>CXF Servlet</display-name>
-        <servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet</servlet-class>
-        <init-param>
-            <param-name>javax.ws.rs.Application</param-name>
-            <param-value>org.apache.shiro.samples.jaxrs.SampleApplication</param-value>
-        </init-param>
-        <load-on-startup>1</load-on-startup>
-    </servlet>
-    <servlet-mapping>
-        <servlet-name>CXFServlet</servlet-name>
-        <url-pattern>/*</url-pattern>
-    </servlet-mapping>
-
-</web-app>
diff --git a/samples/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ContainerIntegrationIT.groovy b/samples/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ContainerIntegrationIT.groovy
deleted file mode 100644
index f55d37d..0000000
--- a/samples/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ContainerIntegrationIT.groovy
+++ /dev/null
@@ -1,143 +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.web.jaxrs
-
-import org.apache.shiro.testing.web.AbstractContainerIT
-import org.junit.Test;
-
-import static com.jayway.restassured.RestAssured.*
-import static org.hamcrest.Matchers.*
-
-public class ContainerIntegrationIT extends AbstractContainerIT {
-
-    @Test
-    void testNoAuthResource() {
-
-        get(getBaseUri() + "say")
-            .then()
-                .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("Hello!"))
-    }
-
-    @Test
-    void testNoAuthResourceAsync() {
-
-        get(getBaseUri() + "say/async")
-                .then()
-                .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("Hello!"))
-    }
-
-    @Test
-    void testSecuredRequiresAuthentication() {
-
-        get(getBaseUri() + "secure/RequiresAuthentication")
-            .then()
-                .assertThat().statusCode(is(401))
-
-        given()
-            .header("Authorization", getBasicAuthorizationHeaderValue("root", "secret"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresAuthentication")
-        .then()
-            .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("protected"))
-    }
-
-    @Test
-    void testSecuredRequiresUser() {
-
-        get(getBaseUri() + "secure/RequiresUser")
-            .then()
-                .assertThat().statusCode(is(401))
-
-        given()
-            .header("Authorization", getBasicAuthorizationHeaderValue("root", "secret"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresUser")
-        .then()
-            .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("protected"))
-    }
-
-    @Test
-    void testSecuredRequiresRoles() {
-
-        get(getBaseUri() + "secure/RequiresRoles")
-            .then()
-                .assertThat().statusCode(is(401))
-
-        given()
-                .header("Authorization", getBasicAuthorizationHeaderValue("guest", "guest"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresRoles")
-        .then()
-            .assertThat()
-                .statusCode(is(403)).and()
-
-        given()
-            .header("Authorization", getBasicAuthorizationHeaderValue("root", "secret"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresRoles")
-        .then()
-            .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("protected"))
-    }
-
-    @Test
-    void testSecuredRequiresPermissions() {
-
-        get(getBaseUri() + "secure/RequiresPermissions")
-            .then()
-                .assertThat().statusCode(is(401))
-
-        given()
-            .header("Authorization", getBasicAuthorizationHeaderValue("guest", "guest"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresPermissions")
-        .then()
-            .assertThat()
-                .statusCode(is(403)).and()
-
-        given()
-            .header("Authorization", getBasicAuthorizationHeaderValue("lonestarr", "vespa"))
-        .when()
-            .get(getBaseUri() + "secure/RequiresPermissions")
-        .then()
-            .assertThat()
-                .statusCode(is(200)).and()
-                .body(equalTo("protected"))
-    }
-
-    @Test
-    void testSecuredRequiresGuest() {
-
-        get(getBaseUri() + "secure/RequiresGuest")
-            .then()
-                .assertThat()
-                    .statusCode(is(200)).and()
-                    .body(equalTo("not protected"))
-    }
-
-}
diff --git a/samples/pom.xml b/samples/pom.xml
index 3ae24a3..a62eff2 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -48,7 +48,6 @@
         <module>guice</module>
         <module>quickstart-guice</module>
         <module>servlet-plugin</module>
-        <module>jaxrs</module>
     </modules>
 
     <reporting>
diff --git a/support/aspectj/pom.xml b/support/aspectj/pom.xml
index 68b1a87..49262ed 100644
--- a/support/aspectj/pom.xml
+++ b/support/aspectj/pom.xml
@@ -62,7 +62,7 @@
     <build>
         <plugins>
             <plugin>
-                <groupId>com.nickwongdev</groupId>
+                <groupId>dev.aspectj</groupId>
                 <artifactId>aspectj-maven-plugin</artifactId>
                 <!-- Using a fork, until such time that the aspect-maven-plugin updates to support JDK 11 - https://github.com/mojohaus/aspectj-maven-plugin/pull/45
                 <groupId>org.codehaus.mojo</groupId>
diff --git a/support/jaxrs/pom.xml b/support/jaxrs/pom.xml
index 20f6d0b..eb97308 100644
--- a/support/jaxrs/pom.xml
+++ b/support/jaxrs/pom.xml
@@ -63,6 +63,42 @@
             <version>2.3.1</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <version>3.4.3</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.cxf</groupId>
+                    <artifactId>cxf-rt-security</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.cxf</groupId>
+                    <artifactId>cxf-rt-transports-http</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>jakarta.xml.soap</groupId>
+                    <artifactId>jakarta.xml.soap-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.sun.activation</groupId>
+                    <artifactId>jakarta.activation</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.sun.xml.messaging.saaj</groupId>
+                    <artifactId>saaj-impl</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.jboss.spec.javax.rmi</groupId>
+                    <artifactId>jboss-rmi-api_1.0_spec</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.woodstox</groupId>
+                    <artifactId>woodstox-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ShiroFeature.java b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ShiroFeature.java
index 0a4718b..4b098b7 100644
--- a/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ShiroFeature.java
+++ b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ShiroFeature.java
@@ -18,6 +18,8 @@
  */
 package org.apache.shiro.web.jaxrs;
 
+import org.apache.shiro.authz.UnauthenticatedException;
+
 import javax.ws.rs.core.Application;
 import javax.ws.rs.core.Feature;
 import javax.ws.rs.core.FeatureContext;
@@ -25,7 +27,7 @@
 
 
 /**
- * Shiro JAX-RS feature which includes {@link ExceptionMapper}, {@link SubjectPrincipalRequestFilter}, and
+ * Shiro JAX-RS feature which includes {@link UnauthorizedExceptionExceptionMapper}, {@link SubjectPrincipalRequestFilter}, and
  * {@link ShiroAnnotationFilterFeature}.
  *
  * Typically a JAX-RS {@link Application} class will include this Feature class in the
@@ -52,7 +54,8 @@
     @Override
     public boolean configure(FeatureContext context) {
 
-        context.register(ExceptionMapper.class);
+        context.register(UnauthorizedExceptionExceptionMapper.class);
+        context.register(UnauthenticatedException.class);
         context.register(SubjectPrincipalRequestFilter.class);
         context.register(ShiroAnnotationFilterFeature.class);
 
diff --git a/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthenticatedExceptionExceptionMapper.java b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthenticatedExceptionExceptionMapper.java
new file mode 100644
index 0000000..872bd58
--- /dev/null
+++ b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthenticatedExceptionExceptionMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.web.jaxrs;
+
+
+import org.apache.shiro.authz.UnauthenticatedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+
+/**
+ * JAX-RS exception mapper used to map Shiro {@link UnauthenticatedException} to HTTP status codes.
+ * {@link UnauthenticatedException} will be mapped to 403.
+ * @since 1.4
+ */
+public class UnauthenticatedExceptionExceptionMapper implements ExceptionMapper<UnauthenticatedException> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(UnauthenticatedExceptionExceptionMapper.class);
+
+    @Override
+    public Response toResponse(UnauthenticatedException exception) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("unauthenticated.", exception);
+        }
+
+        return Response.status(Status.FORBIDDEN).build();
+    }
+}
diff --git a/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ExceptionMapper.java b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapper.java
similarity index 60%
rename from support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ExceptionMapper.java
rename to support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapper.java
index ec6fb64..d6e842b 100644
--- a/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/ExceptionMapper.java
+++ b/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapper.java
@@ -19,30 +19,30 @@
 package org.apache.shiro.web.jaxrs;
 
 
-import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.authz.UnauthorizedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
 
 /**
- * JAX-RS exception mapper used to map Shiro {@link AuthorizationExceptions} to HTTP status codes.
- * {@link UnauthorizedException} will be mapped to 403, all others 401.
+ * JAX-RS exception mapper used to map Shiro {@link UnauthorizedException} to HTTP status codes.
+ * {@link UnauthorizedException} will be mapped to 401.
  * @since 1.4
  */
-public class ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<AuthorizationException> {
+public class UnauthorizedExceptionExceptionMapper implements ExceptionMapper<UnauthorizedException> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(UnauthorizedExceptionExceptionMapper.class);
 
     @Override
-    public Response toResponse(AuthorizationException exception) {
+    public Response toResponse(UnauthorizedException exception) {
 
-        Status status;
-
-        if (exception instanceof UnauthorizedException) {
-            status = Status.FORBIDDEN;
-        } else {
-            status = Status.UNAUTHORIZED;
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("unauthenticated.", exception);
         }
 
-        return Response.status(status).build();
+        return Response.status(Status.UNAUTHORIZED).build();
     }
 }
diff --git a/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ExceptionMapperTest.groovy b/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ExceptionMapperTest.groovy
deleted file mode 100644
index f42fbd6..0000000
--- a/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/ExceptionMapperTest.groovy
+++ /dev/null
@@ -1,64 +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.web.jaxrs
-
-import org.apache.shiro.authz.AuthorizationException
-import org.apache.shiro.authz.UnauthorizedException
-import org.junit.Test
-
-import javax.ws.rs.core.Response
-import javax.ws.rs.ext.RuntimeDelegate
-
-import static org.junit.Assert.assertSame
-import static org.mockito.Mockito.*
-
-/**
- * Tests for {@link ExceptionMapper}.
- * @since 1.4
- */
-class ExceptionMapperTest {
-
-    @Test
-    void testUnauthorizedException() {
-
-        doTest(new UnauthorizedException("expected test exception."), Response.Status.FORBIDDEN)
-        doTest(new AuthorizationException("expected test exception."), Response.Status.UNAUTHORIZED)
-        doTest(null, Response.Status.UNAUTHORIZED)
-    }
-
-    private void doTest(AuthorizationException exception , Response.StatusType expectedStatus) {
-        def runtimeDelegate = mock(RuntimeDelegate)
-
-        RuntimeDelegate.setInstance(runtimeDelegate)
-
-        def responseBuilder = mock(Response.ResponseBuilder)
-        def response = mock(Response)
-
-        when(runtimeDelegate.createResponseBuilder()).then(args -> responseBuilder)
-        when(responseBuilder.status((Response.StatusType) expectedStatus)).then(args -> responseBuilder)
-        when(responseBuilder.build()).then(args -> response)
-
-        def responseResult = new ExceptionMapper().toResponse(exception)
-        assertSame response, responseResult
-
-        verify(runtimeDelegate).createResponseBuilder()
-        verify(responseBuilder).status((Response.StatusType) expectedStatus)
-        verify(responseBuilder).build()
-    }
-}
diff --git a/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapperTest.groovy b/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapperTest.groovy
new file mode 100644
index 0000000..ef64f47
--- /dev/null
+++ b/support/jaxrs/src/test/groovy/org/apache/shiro/web/jaxrs/UnauthorizedExceptionExceptionMapperTest.groovy
@@ -0,0 +1,49 @@
+/*
+ * 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.web.jaxrs
+
+import org.apache.shiro.authz.AuthorizationException
+import org.apache.shiro.authz.HostUnauthorizedException
+import org.apache.shiro.authz.UnauthenticatedException
+import org.apache.shiro.authz.UnauthorizedException
+import org.junit.Test
+
+import javax.ws.rs.core.Response
+import javax.ws.rs.ext.ExceptionMapper
+
+import static org.junit.Assert.assertEquals
+
+/**
+ * Tests for {@link UnauthorizedExceptionExceptionMapper}.
+ * @since 1.4
+ */
+class UnauthorizedExceptionExceptionMapperTest {
+
+    @Test
+    void testUnauthorizedException() {
+        doTest(new UnauthorizedException("expected test exception."), Response.Status.UNAUTHORIZED, new UnauthorizedExceptionExceptionMapper())
+        doTest(new HostUnauthorizedException("expected test exception."), Response.Status.UNAUTHORIZED, new UnauthorizedExceptionExceptionMapper())
+        doTest(new UnauthenticatedException("expected test exception."), Response.Status.FORBIDDEN, new UnauthenticatedExceptionExceptionMapper())
+    }
+
+    private static void doTest(AuthorizationException exception , Response.StatusType expectedStatus, ExceptionMapper<? extends Throwable> exceptionMapper) {
+        final var response = exceptionMapper.toResponse(exception);
+        assertEquals(expectedStatus.statusCode, response.status);
+    }
+}
diff --git a/support/spring-boot/spring-boot-starter/pom.xml b/support/spring-boot/spring-boot-starter/pom.xml
index f797e6d..100d3ca 100644
--- a/support/spring-boot/spring-boot-starter/pom.xml
+++ b/support/spring-boot/spring-boot-starter/pom.xml
@@ -46,6 +46,16 @@
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
             <optional>true</optional>
         </dependency>
diff --git a/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.java b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.java
index 6c00d29..6b00e51 100644
--- a/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.java
+++ b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.java
@@ -21,6 +21,7 @@
 import org.apache.shiro.mgt.SecurityManager;
 import org.apache.shiro.spring.config.AbstractShiroAnnotationProcessorConfiguration;
 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
+import org.springframework.aop.config.AopConfigUtils;
 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -38,7 +39,9 @@
 
     @Bean
     @DependsOn("lifecycleBeanPostProcessor")
-    @ConditionalOnMissingBean
+    @ConditionalOnMissingBean(
+            value = DefaultAdvisorAutoProxyCreator.class, name = AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME
+    )
     @Override
     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
         return super.defaultAdvisorAutoProxyCreator();
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java
similarity index 96%
rename from support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java
rename to support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java
index 3b89b63..49732ae 100644
--- a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java
+++ b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.java
@@ -41,6 +41,7 @@
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -50,6 +51,7 @@
 @Configuration
 @AutoConfigureBefore(ShiroAutoConfiguration.class)
 @AutoConfigureAfter(ShiroWebMvcAutoConfiguration.class)
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 @ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
 public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
 
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
similarity index 94%
rename from support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
rename to support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
index 81a11ec..05af40c 100644
--- a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
+++ b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
@@ -24,6 +24,7 @@
 import org.apache.shiro.web.servlet.AbstractShiroFilter;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -34,6 +35,7 @@
  * @since 1.4.0
  */
 @Configuration
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 @ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
 public class ShiroWebFilterConfiguration extends AbstractShiroWebFilterConfiguration {
 
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java
similarity index 90%
rename from support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java
rename to support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java
index 26fdeb7..bd30c1b 100644
--- a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java
+++ b/support/spring-boot/spring-boot-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebMvcAutoConfiguration.java
@@ -21,6 +21,7 @@
 import org.apache.shiro.spring.web.config.ShiroRequestMappingConfig;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@@ -28,5 +29,6 @@
 @Configuration
 @ConditionalOnClass(RequestMappingHandlerMapping.class)
 @Import(ShiroRequestMappingConfig.class)
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 @ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
 public class ShiroWebMvcAutoConfiguration { }
diff --git a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 3737aac..5ef2115 100644
--- a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -11,6 +11,42 @@
       "type": "java.lang.Boolean",
       "description": "A boolean flag that can disable all Shiro Spring Boot starters.  This is mostly useful during testing or debugging, or if you want to compare behavior when Shiro is enabled or disabled.",
       "defaultValue": true
+    },
+    {
+      "name": "shiro.web.enabled",
+      "type": "java.lang.Boolean",
+      "description": "A boolean flag that can disable all Shiro Spring Boot starters.  This is mostly useful during testing or debugging, or if you want to compare behavior when Shiro is enabled or disabled.",
+      "defaultValue": true
+    },
+    {
+      "name": "shiro.loginUrl",
+      "type": "java.lang.String",
+      "description": "The application's login URL to be assigned to all acquired Filters that subclass AccessControlFilter or 'null' if no value should be assigned globally.",
+      "defaultValue": "/login.jsp"
+    },
+    {
+      "name": "shiro.successUrl",
+      "type": "java.lang.String",
+      "description": "The application's after-login success URL to be assigned to all acquired Filters that subclass AuthenticationFilter or null if no value should be assigned globally.",
+      "defaultValue": "/"
+    },
+    {
+      "name": "shiro.unauthorizedUrl",
+      "type": "java.lang.String",
+      "description": "The application's 'unauthorized' URL to apply to as a convenience to all discovered AuthorizationFilter instances.",
+      "defaultValue": null
+    },
+    {
+      "name": "shiro.sessionManager.sessionIdCookieEnabled",
+      "type": "java.lang.String",
+      "description": "Enable or disable session tracking via a cookie.",
+      "defaultValue": true
+    },
+    {
+      "name": "shiro.sessionManager.sessionIdUrlRewritingEnabled",
+      "type": "java.lang.String",
+      "description": "Enable or disable session tracking via a URL parameter.  If your site requires cookies, it is recommended you disable this.",
+      "defaultValue": true
     }
   ]
 }
\ No newline at end of file
diff --git a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.factories b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.factories
index ac5e856..9743fcc 100644
--- a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.factories
+++ b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -1,4 +1,7 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
+  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
+  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration,\
+  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebMvcAutoConfiguration,\
   org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,\
   org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,\
   org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration
diff --git a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.provides b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.provides
index 1749212..d9b5251 100644
--- a/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.provides
+++ b/support/spring-boot/spring-boot-starter/src/main/resources/META-INF/spring.provides
@@ -1 +1 @@
-provides: shiro
\ No newline at end of file
+provides: shiro,shiro-web
\ No newline at end of file
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy b/support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
similarity index 100%
rename from support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
rename to support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy b/support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
similarity index 100%
rename from support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
rename to support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy b/support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
similarity index 100%
rename from support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
rename to support/spring-boot/spring-boot-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java b/support/spring-boot/spring-boot-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
similarity index 100%
rename from support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
rename to support/spring-boot/spring-boot-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
diff --git a/support/spring-boot/spring-boot-web-starter/pom.xml b/support/spring-boot/spring-boot-web-starter/pom.xml
index 0a69002..4af32ae 100644
--- a/support/spring-boot/spring-boot-web-starter/pom.xml
+++ b/support/spring-boot/spring-boot-web-starter/pom.xml
@@ -60,29 +60,10 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
-            <version>${spring-boot.version}</version>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
-            <version>${spring-boot.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
         </dependency>
 
     </dependencies>
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/NOTICE b/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/NOTICE
deleted file mode 100644
index 9d26a95..0000000
--- a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/NOTICE
+++ /dev/null
@@ -1,15 +0,0 @@
-Apache Shiro
-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 
-available at http://www.javaspecialists.eu/archive/Issue015.html,
-with continued modifications.  
-
-Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
-code for this  product was copied for simplicity and to reduce
-dependencies  from the source code developed by the Spring Framework
-Project  (http://www.springframework.org).
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
deleted file mode 100644
index ff13832..0000000
--- a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "groups": [
-    {
-      "name": "shiro"
-    }
-  ],
-  "properties": [
-
-    {
-      "name": "shiro.web.enabled",
-      "type": "java.lang.Boolean",
-      "description": "A boolean flag that can disable all Shiro Spring Boot starters.  This is mostly useful during testing or debugging, or if you want to compare behavior when Shiro is enabled or disabled.",
-      "defaultValue": true
-    },
-    {
-      "name": "shiro.loginUrl",
-      "type": "java.lang.String",
-      "description": "The application's login URL to be assigned to all acquired Filters that subclass AccessControlFilter or 'null' if no value should be assigned globally.",
-      "defaultValue": "/login.jsp"
-    },
-    {
-      "name": "shiro.successUrl",
-      "type": "java.lang.String",
-      "description": "The application's after-login success URL to be assigned to all acquired Filters that subclass AuthenticationFilter or null if no value should be assigned globally.",
-      "defaultValue": "/"
-    },
-    {
-      "name": "shiro.unauthorizedUrl",
-      "type": "java.lang.String",
-      "description": "The application's 'unauthorized' URL to apply to as a convenience to all discovered AuthorizationFilter instances.",
-      "defaultValue": null
-    },
-    {
-      "name": "shiro.sessionManager.sessionIdCookieEnabled",
-      "type": "java.lang.String",
-      "description": "Enable or disable session tracking via a cookie.",
-      "defaultValue": true
-    },
-    {
-      "name": "shiro.sessionManager.sessionIdUrlRewritingEnabled",
-      "type": "java.lang.String",
-      "description": "Enable or disable session tracking via a URL parameter.  If your site requires cookies, it is recommended you disable this.",
-      "defaultValue": true
-    }
-  ]
-}
\ No newline at end of file
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.factories b/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.factories
deleted file mode 100644
index 1546fc1..0000000
--- a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.factories
+++ /dev/null
@@ -1,4 +0,0 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
-  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
-  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration,\
-  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebMvcAutoConfiguration
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.provides b/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.provides
deleted file mode 100644
index ffe94a1..0000000
--- a/support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.provides
+++ /dev/null
@@ -1 +0,0 @@
-provides: shiro-web
\ No newline at end of file
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/WebSpringFactoriesTest.groovy b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/WebSpringFactoriesTest.groovy
deleted file mode 100644
index 491bc0a..0000000
--- a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/WebSpringFactoriesTest.groovy
+++ /dev/null
@@ -1,42 +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.spring.boot.autoconfigure.web
-
-import org.junit.Test
-
-import static org.hamcrest.MatcherAssert.assertThat
-import static org.hamcrest.Matchers.matchesPattern
-import static org.hamcrest.Matchers.not
-
-class WebSpringFactoriesTest {
-
-    @Test
-    void springFactoriesConfigContainsNoWhitespace() {
-        Properties props = new Properties()
-        props.load(new FileReader("src/main/resources/META-INF/spring.factories"))
-        assertNoWhitespaceInEntries(props)
-    }
-
-    static private assertNoWhitespaceInEntries(Properties props) {
-        props.each{ key, val ->
-            assertThat "Property [${key}] contains whitespace",
-            props.get("org.springframework.boot.autoconfigure.EnableAutoConfiguration"), not(matchesPattern(".*\\s.*"))
-        }
-    }
-}
diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml
index 9af02f8..94da7d9 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>
@@ -58,25 +73,20 @@
     <build>
         <plugins>
             <plugin>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.1.0</version>
-                <configuration>
-                    <descriptors>
-                        <descriptor>src/main/assembly/cli.assembly.xml</descriptor>
-                    </descriptors>
-                    <archive>
-                        <manifest>
-                            <mainClass>org.apache.shiro.tools.hasher.Hasher</mainClass>
-                        </manifest>
-                    </archive>
-                </configuration>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.12.RELEASE</version>
                 <executions>
                     <execution>
-                        <id>make-assembly</id>
-                        <phase>package</phase>
                         <goals>
-                            <goal>single</goal>
+                            <goal>repackage</goal>
                         </goals>
+                        <configuration>
+                            <mainClass>org.apache.shiro.tools.hasher.Hasher</mainClass>
+                            <classifier>cli</classifier>
+                            <attach>true</attach>
+                            <layout>JAR</layout>
+                        </configuration>
                     </execution>
                 </executions>
             </plugin>
diff --git a/tools/hasher/src/main/assembly/cli.assembly.xml b/tools/hasher/src/main/assembly/cli.assembly.xml
deleted file mode 100644
index 9926c41..0000000
--- a/tools/hasher/src/main/assembly/cli.assembly.xml
+++ /dev/null
@@ -1,35 +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.
-  -->
-<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
-  <id>cli</id>
-  <formats>
-    <format>jar</format>
-  </formats>
-  <includeBaseDirectory>false</includeBaseDirectory>
-  <dependencySets>
-    <dependencySet>
-      <outputDirectory>/</outputDirectory>
-      <useProjectArtifact>true</useProjectArtifact>
-      <unpack>true</unpack>
-      <scope>runtime</scope>
-    </dependencySet>
-  </dependencySets>
-</assembly>
\ No newline at end of file
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 @@
 
 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.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 @@
  */
 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 @@
             }
 
             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 @@
 
             String output = format.format(hash);
 
-            System.out.println(output);
+            LOG.info(output);
 
         } catch (IllegalArgumentException iae) {
             exit(iae, debug);
@@ -339,16 +347,16 @@
 
     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 @@
                 "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 @@
                 "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 @@
         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/samples/jaxrs/src/main/resources/logback.xml b/tools/hasher/src/main/resources/logback.xml
similarity index 75%
rename from samples/jaxrs/src/main/resources/logback.xml
rename to tools/hasher/src/main/resources/logback.xml
index 6f20d75..502d9d1 100644
--- a/samples/jaxrs/src/main/resources/logback.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,18 +16,16 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
+
 <configuration>
 
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
-            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>[%-5level] %msg%n</pattern>
         </encoder>
     </appender>
 
-    <root level="INFO">
-        <appender-ref ref="STDOUT" />
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
     </root>
-
-    <logger name="org.apache.shiro.web.jaxrs" level="INFO"/>
-    <logger name="org.eclipse.jetty.annotations.AnnotationParser" level="ERROR"/>
-</configuration>
\ No newline at end of file
+</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/samples/jaxrs/src/main/resources/logback.xml b/tools/hasher/src/test/resources/logback-test.xml
similarity index 67%
copy from samples/jaxrs/src/main/resources/logback.xml
copy to tools/hasher/src/test/resources/logback-test.xml
index 6f20d75..a652392 100644
--- a/samples/jaxrs/src/main/resources/logback.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,18 +16,13 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
+
 <configuration>
 
-    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
+    <appender name="list" class="ch.qos.logback.core.read.ListAppender">
     </appender>
 
-    <root level="INFO">
-        <appender-ref ref="STDOUT" />
+    <root level="info">
+        <appender-ref ref="list"/>
     </root>
-
-    <logger name="org.apache.shiro.web.jaxrs" level="INFO"/>
-    <logger name="org.eclipse.jetty.annotations.AnnotationParser" level="ERROR"/>
-</configuration>
\ No newline at end of file
+</configuration>