Merge pull request #280 from apache/SHIRO-290

[SHIRO-290] Implement BCrypt and Argon2
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..208a054
--- /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 :: Hashes :: 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/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..4b3b138
--- /dev/null
+++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,18 @@
+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.
+
+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
+dependencies  from the source code developed by the Spring Framework
+Project  (http://www.springframework.org).
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..24924d7
--- /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 :: Hashes :: 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/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..4b3b138
--- /dev/null
+++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,18 @@
+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.
+
+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
+dependencies  from the source code developed by the Spring Framework
+Project  (http://www.springframework.org).
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..a66da46 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>
@@ -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>
@@ -1224,6 +1235,12 @@
                 <artifactId>junit-servers-jetty</artifactId>
                 <version>${junit.server.jetty.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>org.bouncycastle</groupId>
+                <artifactId>bcprov-jdk15on</artifactId>
+                <version>${bouncycastle.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
     
diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml
index 9af02f8..2a3b4aa 100644
--- a/tools/hasher/pom.xml
+++ b/tools/hasher/pom.xml
@@ -44,13 +44,28 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- explicitly use the compile scopes for the algorithms, so we can access the parameter names. -->
+        <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-argon2</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shiro.crypto</groupId>
+            <artifactId>shiro-hashes-bcrypt</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <dependency>
             <groupId>commons-cli</groupId>
             <artifactId>commons-cli</artifactId>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
             <scope>runtime</scope>
         </dependency>
     </dependencies>
diff --git a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
index e203153..020d6d4 100644
--- a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
+++ b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
@@ -20,13 +20,11 @@
 
 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/tools/hasher/src/main/resources/logback.xml b/tools/hasher/src/main/resources/logback.xml
new file mode 100644
index 0000000..502d9d1
--- /dev/null
+++ b/tools/hasher/src/main/resources/logback.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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.
+  -->
+
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%-5level] %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java
new file mode 100644
index 0000000..00e6286
--- /dev/null
+++ b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.shiro.tools.hasher;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @since 2.0
+ */
+public class HasherTest {
+
+    private final InputStream systemIn = System.in;
+
+    private ByteArrayInputStream testIn;
+
+    private final Logger hasherToolLogger = (Logger) LoggerFactory.getLogger("ROOT");
+    private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
+
+    @BeforeEach
+    public void setUpOutput() {
+        hasherToolLogger.detachAndStopAllAppenders();
+        hasherToolLogger.addAppender(listAppender);
+        listAppender.start();
+    }
+
+    private void provideInput(String data) {
+        testIn = new ByteArrayInputStream(data.getBytes());
+        System.setIn(testIn);
+    }
+
+    @AfterEach
+    public void restoreSystemInputOutput() throws IOException {
+        System.setIn(systemIn);
+        testIn.close();
+        listAppender.stop();
+    }
+
+
+    @Test
+    public void testArgon2Hash() {
+        // given
+        String[] args = {"--debug", "--password", "--pnoconfirm"};
+        provideInput("secret#shiro,password;Jo8opech");
+
+        // when
+        Hasher.main(args);
+        List<ILoggingEvent> loggingEvents = listAppender.list;
+
+        // when
+        assertEquals(1, loggingEvents.size());
+        ILoggingEvent iLoggingEvent = loggingEvents.get(0);
+        assertTrue(iLoggingEvent.getMessage().contains("$shiro2$argon2id$v=19"));
+    }
+
+    @Test
+    public void testBCryptHash() {
+        // given
+        String[] args = {"--debug", "--password", "--pnoconfirm", "--algorithm", "2y"};
+        provideInput("secret#shiro,password;Jo8opech");
+
+        // when
+        Hasher.main(args);
+        List<ILoggingEvent> loggingEvents = listAppender.list;
+
+        // when
+        assertEquals(1, loggingEvents.size());
+        ILoggingEvent iLoggingEvent = loggingEvents.get(0);
+        assertTrue(iLoggingEvent.getMessage().contains("$shiro2$2y$10$"));
+    }
+}
diff --git a/tools/hasher/src/test/resources/logback-test.xml b/tools/hasher/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a652392
--- /dev/null
+++ b/tools/hasher/src/test/resources/logback-test.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.
+  -->
+
+<configuration>
+
+    <appender name="list" class="ch.qos.logback.core.read.ListAppender">
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="list"/>
+    </root>
+</configuration>