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
- * "Why add salt?" 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.
+ * "Why add salt?" 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>