From d9c656a9f5b5d5c66b89f98158ed2933fa552d4f Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Thu, 31 Dec 2020 10:45:35 +0100 Subject: [PATCH 01/11] [SHIRO-290] Global BouncyCastle version --- crypto/cipher/pom.xml | 1 - pom.xml | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crypto/cipher/pom.xml b/crypto/cipher/pom.xml index 72974d4614..2b03bfd2b8 100644 --- a/crypto/cipher/pom.xml +++ b/crypto/cipher/pom.xml @@ -65,7 +65,6 @@ org.bouncycastle bcprov-jdk15on - 1.64 test diff --git a/pom.xml b/pom.xml index 41d098192e..e7e580a094 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,7 @@ 4.2.2 2.1.6 2.39.0 + 1.68 4.0.2 @@ -1223,6 +1224,12 @@ junit-servers-jetty ${junit.server.jetty.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + From 3ab005d26a51c23b9761a012e16475449479fa78 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Thu, 31 Dec 2020 13:58:35 +0100 Subject: [PATCH 02/11] [SHIRO-290] WIP: Implement Unix crypt format, starting with bcrypt. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TBD: HashRequest - TBD: PasswortMatcher doesn’t know about the new hash format yet --- .../credential/DefaultPasswordService.java | 19 +- .../realm/text/TextConfigurationRealm.java | 1 + .../credential/PasswordMatcherTest.groovy | 19 ++ crypto/hash/pom.xml | 5 + .../shiro/crypto/hash/AbstractCryptHash.java | 124 +++++++++++ .../apache/shiro/crypto/hash/BCryptHash.java | 135 ++++++++++++ .../crypto/hash/format/Base64Format.java | 7 +- .../hash/format/DefaultHashFormatFactory.java | 9 +- .../shiro/crypto/hash/format/HashFormat.java | 4 +- .../crypto/hash/format/HashFormatFactory.java | 2 +- .../shiro/crypto/hash/format/HexFormat.java | 7 +- .../hash/format/ModularCryptFormat.java | 4 +- .../hash/format/ParsableHashFormat.java | 2 +- .../hash/format/ProvidedHashFormat.java | 13 +- .../crypto/hash/format/Shiro1CryptFormat.java | 47 +++-- .../crypto/hash/format/UnixCryptFormat.java | 127 ++++++++++++ .../hash/src/main/resources/META-INF/NOTICE | 5 +- .../shiro/crypto/hash/BCryptHashTest.groovy | 59 ++++++ .../DefaultHashFormatFactoryTest.groovy | 13 ++ .../hash/format/ProvidedHashFormatTest.groovy | 3 +- .../hash/format/ToStringHashFormat.groovy | 7 +- .../hash/format/UnixCryptFormatTest.groovy | 108 ++++++++++ .../shiro/lang/codec/OpenBSDBase64.java | 194 ++++++++++++++++++ .../apache/shiro/lang/util/StringUtils.java | 12 ++ 24 files changed, 875 insertions(+), 51 deletions(-) create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java create mode 100644 crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy create mode 100644 crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy create mode 100644 lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java 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 ea12668e4f..566376280c 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,17 +18,21 @@ */ 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.Shiro1CryptFormat; import org.apache.shiro.lang.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.MessageDigest; + /** * Default implementation of the {@link PasswordService} interface that relies on an internal * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function: @@ -66,12 +70,14 @@ public DefaultPasswordService() { this.hashFormatFactory = new DefaultHashFormatFactory(); } + @Override public String encryptPassword(Object plaintext) { Hash hash = hashPassword(plaintext); checkHashFormatDurability(); return this.hashFormat.format(hash); } + @Override public Hash hashPassword(Object plaintext) { ByteSource plaintextBytes = createByteSource(plaintext); if (plaintextBytes == null || plaintextBytes.isEmpty()) { @@ -81,6 +87,7 @@ public Hash hashPassword(Object plaintext) { return hashService.computeHash(request); } + @Override public boolean passwordsMatch(Object plaintext, Hash saved) { ByteSource plaintextBytes = createByteSource(plaintext); @@ -94,7 +101,8 @@ public boolean passwordsMatch(Object plaintext, Hash saved) { HashRequest request = buildHashRequest(plaintextBytes, saved); - Hash computed = this.hashService.computeHash(request); + // TODO: hashrequest for (b)crypt + final Hash computed = this.hashService.computeHash(request); return constantEquals(saved.toString(), computed.toString()); } @@ -133,6 +141,7 @@ protected ByteSource createByteSource(Object o) { return ByteSource.Util.bytes(o); } + @Override public boolean passwordsMatch(Object submittedPlaintext, String saved) { ByteSource plaintextBytes = createByteSource(submittedPlaintext); @@ -153,7 +162,7 @@ public boolean passwordsMatch(Object submittedPlaintext, String saved) { if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) { - ParsableHashFormat parsableHashFormat = (ParsableHashFormat)discoveredFormat; + ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat; Hash savedHash = parsableHashFormat.parse(saved); return passwordsMatch(submittedPlaintext, savedHash); 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 0439f93bb1..53d16ed756 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 @@ protected void processUserDefinitions(Map userDefs) { 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/PasswordMatcherTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy index 59d5530935..85b75e7e02 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,7 +20,10 @@ package org.apache.shiro.authc.credential import org.apache.shiro.authc.AuthenticationInfo import org.apache.shiro.authc.AuthenticationToken +import org.apache.shiro.authc.SimpleAuthenticationInfo +import org.apache.shiro.authc.UsernamePasswordToken import org.apache.shiro.crypto.hash.Sha256Hash +import org.apache.shiro.crypto.hash.format.UnixCryptFormat import org.junit.Test import static org.easymock.EasyMock.* @@ -175,7 +178,23 @@ class PasswordMatcherTest { } verify token, info, service + } + @Test + void testBCryptPassword() { + // given + def matcher = new PasswordMatcher(); + def bcryptPw = '$unixcrypt$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + def bcryptHash = new UnixCryptFormat().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) + + assertTrue match } } diff --git a/crypto/hash/pom.xml b/crypto/hash/pom.xml index 6526345c27..e5503afab8 100644 --- a/crypto/hash/pom.xml +++ b/crypto/hash/pom.xml @@ -61,6 +61,11 @@ org.apache.shiro shiro-crypto-core + + + org.bouncycastle + bcprov-jdk15on + 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 0000000000..5961f1562a --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java @@ -0,0 +1,124 @@ +/* + * 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.CodecSupport; +import org.apache.shiro.lang.codec.Hex; + +import java.io.Serializable; +import java.security.MessageDigest; +import java.util.Arrays; + +public abstract class AbstractCryptHash extends CodecSupport implements Hash, Serializable { + + private static final long serialVersionUID = 2483214646921027859L; + /** + * 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; + + /** + * Implemented by subclasses, this specifies the {@link MessageDigest MessageDigest} algorithm name + * to use when performing the hash. + * + * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash. + */ + @Override + public abstract String getAlgorithmName(); + + public abstract int getSaltLength(); + + + /** + * Returns a hex-encoded string of the underlying {@link #getBytes byte array}. + *

+ * This implementation caches the resulting hex string so multiple calls to this method remain efficient. + * + * @return a hex-encoded string of the underlying {@link #getBytes byte array}. + */ + @Override + public String toHex() { + if (this.hexEncoded == null) { + this.hexEncoded = Hex.encodeToString(this.getBytes()); + } + return this.hexEncoded; + } + + /** + * Returns a Base64-encoded string of the underlying {@link #getBytes byte array}. + *

+ * This implementation caches the resulting Base64 string so multiple calls to this method remain efficient. + * + * @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. + this.base64Encoded = Base64.encodeToString(this.getBytes()); + } + return this.base64Encoded; + } + + /** + * Simple implementation that merely returns {@link #toHex() toHex()}. + * + * @return the {@link #toHex() toHex()} value. + */ + @Override + public String toString() { + return this.toHex(); + } + + /** + * Returns {@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. + * + * @param other the object (Hash) to check for equality. + * @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(final Object other) { + if (other instanceof Hash) { + final Hash that = (Hash) other; + return MessageDigest.isEqual(this.getBytes(), that.getBytes()); + } + return false; + } + + /** + * Simply returns toHex().hashCode(); + * + * @return toHex().hashCode() + */ + @Override + public int hashCode() { + if (this.getBytes() == null || this.getBytes().length == 0) { + return 0; + } + return Arrays.hashCode(this.getBytes()); + } +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java new file mode 100644 index 0000000000..da2098541f --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -0,0 +1,135 @@ +/* + * 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.OpenBSDBase64; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.StringJoiner; + +public class BCryptHash extends AbstractCryptHash { + + private static final long serialVersionUID = 6957869292324606101L; + + protected static final int DEFAULT_ITERATIONS = 10; + + private static final String ALGORITHM_NAME = "2y"; + + private static final int SALT_LENGTH = 16; + + private final String version; + private final byte[] salt; + private final byte[] hashedData; + private final int cost; + + + public BCryptHash(final byte[] salt, final byte[] hashedData, final int cost) { + this(ALGORITHM_NAME, salt, hashedData, cost); + } + + public BCryptHash(final String version, final byte[] salt, final byte[] hashedData, final int cost) { + super(); + this.version = version; + this.salt = salt; + this.hashedData = hashedData; + this.cost = cost; + } + + public static BCryptHash generate(final char[] source) { + return generate(source, createSalt(), DEFAULT_ITERATIONS); + } + + + public static BCryptHash generate(final char[] source, final byte[] initialSalt, final int cost) { + final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, source, initialSalt, cost); + + final String dataSection = cryptString.substring(cryptString.lastIndexOf('$') + 1); + 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(ALGORITHM_NAME, salt, hashedData, cost); + } + + protected static byte[] createSalt() { + return new SecureRandom().generateSeed(SALT_LENGTH); + } + + @Override + public String getAlgorithmName() { + return ALGORITHM_NAME; + } + + @Override + public int getSaltLength() { + return SALT_LENGTH; + } + + @Override + public ByteSource getSalt() { + return new SimpleByteSource(Arrays.copyOf(this.salt, SALT_LENGTH)); + } + + /** + * Warning! The returned value is actually the cost, not the iterations. + * + * @return the cost. + */ + @Override + public int getIterations() { + return this.getCost(); + } + + public int getRealIterations() { + return (int) Math.pow(2, this.getCost()); + } + + public int getCost() { + return this.cost; + } + + @Override + public byte[] getBytes() { + return Arrays.copyOf(this.hashedData, this.hashedData.length); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("version='" + this.version + "'") + .add("salt=" + Arrays.toString(this.salt)) + .add("hashedData=" + Arrays.toString(this.hashedData)) + .add("cost=" + this.cost) + .toString(); + } +} 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 78742c0c5c..94088acbe7 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 @@ -18,7 +18,7 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.SimpleHash; /** * {@code HashFormat} that outputs only the hash's digest bytes in Base64 format. It does not print out @@ -27,7 +27,7 @@ * * @since 1.2 */ -public class Base64Format implements HashFormat { +public class Base64Format implements HashFormat { /** * Returns {@code hash != null ? hash.toBase64() : null}. @@ -35,7 +35,8 @@ public class Base64Format implements HashFormat { * @param hash the hash instance to format into a String. * @return {@code hash != null ? hash.toBase64() : null}. */ - public String format(Hash hash) { + @Override + public String format(final SimpleHash hash) { return hash != null ? hash.toBase64() : null; } } 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 34553d9b16..02f080555d 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java @@ -111,13 +111,14 @@ public void setSearchPackages(Set searchPackages) { this.searchPackages = searchPackages; } - public HashFormat getInstance(String in) { + @Override + public HashFormat getInstance(String in) { if (in == null) { return null; } - HashFormat hashFormat = null; - Class clazz = null; + HashFormat hashFormat = null; + Class clazz = null; //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance //optimization. If the input arg is an MCF-formatted string, there will be many unnecessary ClassLoader @@ -128,7 +129,7 @@ public HashFormat getInstance(String in) { 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 c65ae78b5f..700e2f071b 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 @@ -33,7 +33,7 @@ * * @since 1.2 */ -public interface HashFormat { +public interface HashFormat { /** * Returns a formatted string representing the specified Hash instance. @@ -41,5 +41,5 @@ public interface HashFormat { * @param hash the hash instance to format into a String. * @return a formatted string representing the specified Hash instance. */ - String format(Hash hash); + String format(T hash); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java index fa52691098..5cc6b2583d 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java @@ -23,5 +23,5 @@ */ public interface HashFormatFactory { - HashFormat getInstance(String token); + HashFormat getInstance(String token); } 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 5730ac990c..8126a0bb23 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 @@ -18,7 +18,7 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.SimpleHash; /** * {@code HashFormat} that outputs only The hash's digest bytes in hex format. It does not print out @@ -27,7 +27,7 @@ * * @since 1.2 */ -public class HexFormat implements HashFormat { +public class HexFormat implements HashFormat { /** * Returns {@code hash != null ? hash.toHex() : null}. @@ -35,7 +35,8 @@ public class HexFormat implements HashFormat { * @param hash the hash instance to format into a String. * @return {@code hash != null ? hash.toHex() : null}. */ - public String format(Hash hash) { + @Override + public String format(final SimpleHash hash) { return hash != null ? hash.toHex() : null; } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java index ce4917556e..10847d9858 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java @@ -18,6 +18,8 @@ */ package org.apache.shiro.crypto.hash.format; +import org.apache.shiro.crypto.hash.Hash; + /** * A {@code HashFormat} that supports * Modular Crypt Format token rules. @@ -26,7 +28,7 @@ * @see MCF Journal Entry * @since 1.2 */ -public interface ModularCryptFormat extends HashFormat { +public interface ModularCryptFormat extends HashFormat { public static final String TOKEN_DELIMITER = "$"; diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java index 045756805b..af3a0de930 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java @@ -31,7 +31,7 @@ * * @since 1.2 */ -public interface ParsableHashFormat extends HashFormat { +public interface ParsableHashFormat extends HashFormat { /** * Parses the specified formatted string and returns the corresponding Hash instance. 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 3813123217..16f559ea61 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java @@ -40,11 +40,16 @@ public enum ProvidedHashFormat { /** * Value representing the {@link Shiro1CryptFormat} implementation. */ - SHIRO1(Shiro1CryptFormat.class); + SHIRO1(Shiro1CryptFormat.class), + + /** + * Unix Crypt format {@link UnixCryptFormat} as they are used in {@code /etc/shadow} files. + */ + UNIXCRYPT(UnixCryptFormat.class); private final Class clazz; - private ProvidedHashFormat(Class clazz) { + private ProvidedHashFormat(final Class clazz) { this.clazz = clazz; } @@ -52,7 +57,7 @@ Class getHashFormatClass() { return this.clazz; } - public static ProvidedHashFormat byId(String id) { + public static ProvidedHashFormat byId(final String id) { if (id == null) { return null; } @@ -60,7 +65,7 @@ public static ProvidedHashFormat byId(String id) { // 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 24966ea4db..deface27fd 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,9 @@ */ 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.lang.codec.Base64; import org.apache.shiro.lang.util.ByteSource; import org.apache.shiro.lang.util.StringUtils; @@ -85,7 +85,7 @@ * * @since 1.2 */ -public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat { +public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat { public static final String ID = "shiro1"; public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER; @@ -93,19 +93,21 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat public Shiro1CryptFormat() { } + @Override public String getId() { return ID; } - public String format(Hash hash) { + @Override + public String format(final SimpleHash hash) { if (hash == null) { return null; } - String algorithmName = hash.getAlgorithmName(); - ByteSource salt = hash.getSalt(); - int iterations = hash.getIterations(); - StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); + final String algorithmName = hash.getAlgorithmName(); + final ByteSource salt = hash.getSalt(); + final int iterations = hash.getIterations(); + final StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); if (salt != null) { sb.append(salt.toBase64()); @@ -117,44 +119,45 @@ public String format(Hash hash) { return sb.toString(); } - public Hash parse(String formatted) { + @Override + public Hash parse(final String formatted) { if (formatted == null) { return null; } if (!formatted.startsWith(MCF_PREFIX)) { //TODO create a HashFormatException class - String msg = "The argument is not a valid '" + ID + "' formatted hash."; + final String msg = "The argument is not a valid '" + ID + "' formatted hash."; throw new IllegalArgumentException(msg); } - String suffix = formatted.substring(MCF_PREFIX.length()); - String[] parts = suffix.split("\\$"); + final String suffix = formatted.substring(MCF_PREFIX.length()); + final String[] parts = suffix.split("\\$"); //last part is always the digest/checksum, Base64-encoded: - int i = parts.length-1; - String digestBase64 = parts[i--]; + int i = parts.length - 1; + final 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]; + final String saltBase64 = parts[i--]; + final String iterationsString = parts[i--]; + final String algorithmName = parts[i]; - byte[] digest = Base64.decode(digestBase64); + final byte[] digest = Base64.decode(digestBase64); ByteSource salt = null; if (StringUtils.hasLength(saltBase64)) { - byte[] saltBytes = Base64.decode(saltBase64); + final byte[] saltBytes = Base64.decode(saltBase64); salt = ByteSource.Util.bytes(saltBytes); } - int iterations; + final int iterations; try { iterations = Integer.parseInt(iterationsString); - } catch (NumberFormatException e) { - String msg = "Unable to parse formatted hash string: " + formatted; + } catch (final NumberFormatException e) { + final String msg = "Unable to parse formatted hash string: " + formatted; throw new IllegalArgumentException(msg, e); } - SimpleHash hash = new SimpleHash(algorithmName); + final SimpleHash hash = new SimpleHash(algorithmName); hash.setBytes(digest); if (salt != null) { hash.setSalt(salt); diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java new file mode 100644 index 0000000000..66e3202609 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java @@ -0,0 +1,127 @@ +/* + * 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.BCryptHash; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.lang.codec.OpenBSDBase64; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +public class UnixCryptFormat implements ModularCryptFormat, ParsableHashFormat { + + private static final String ID = "unixcrypt"; + private static final List BCRYPT_IDS = Arrays.asList("2", "2a", "2b", "2y"); + private static final List BCRYPT_PREFIXES = createPrefixes(); + + private static final String DELIMITER_CHAR = "$"; + private static final Pattern DELIMITER = Pattern.compile("\\" + DELIMITER_CHAR); + + private static List createPrefixes() { + final List prefixes = BCRYPT_IDS.stream() + .map(id -> TOKEN_DELIMITER + id + TOKEN_DELIMITER) + .collect(Collectors.toList()); + + return Collections.unmodifiableList(prefixes); + } + + @Override + public String getId() { + return ID; + } + + + @Override + public String format(final BCryptHash hash) { + return "$" + ID + this.getCanonicalFormat(hash); + + } + + protected String getCanonicalFormat(final Hash hash) { + final String algorithmName = requireNonNull(hash).getAlgorithmName(); + + if (!BCRYPT_IDS.contains(algorithmName)) { + final String msg = "AlgorithmName [" + algorithmName + "] is not a valid bcrypt algorithm." + + "Allowed values: [" + BCRYPT_IDS + "]."; + throw new IllegalArgumentException(msg); + } + + final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); + final String saltHex = new String(bcryptBase64.encode(hash.getSalt().getBytes()), StandardCharsets.ISO_8859_1); + final byte[] bytes = hash.getBytes(); + final String byteHex = new String(bcryptBase64.encode(bytes), StandardCharsets.ISO_8859_1) + .replace('+', '.'); + + return new StringJoiner(DELIMITER_CHAR, DELIMITER_CHAR, "") + .add(algorithmName) + .add("" + hash.getIterations()) + .add(saltHex + byteHex) + .toString(); + } + + @Override + public Hash parse(final String formatted) { + requireNonNull(formatted); + if (!formatted.startsWith("$" + ID + "$")) { + final String msg = "The argument is not a valid bcrypt formatted hash. " + + "Expected to start with $" + ID + "$ but was " + formatted + "."; + throw new IllegalArgumentException(msg); + } + + final String canonicalFormat = formatted.substring(formatted.indexOf("$", 1)); + + final Optional matchedPrefix = BCRYPT_PREFIXES.stream() + .filter(canonicalFormat::startsWith) + .findAny(); + if (!matchedPrefix.isPresent()) { + final String msg = "The argument is not a valid bcrypt formatted hash. " + + "Expected canconical form to start with any of " + BCRYPT_IDS + + ", but found " + canonicalFormat + "."; + throw new IllegalArgumentException(msg); + } + + final String prefix = matchedPrefix.orElseThrow(NoSuchElementException::new); + final String id = prefix.substring(1, prefix.length() - 1); + final String suffix = canonicalFormat.substring(canonicalFormat.indexOf(DELIMITER_CHAR, 1) + 1); + final String[] parts = DELIMITER.split(suffix); + + final String costString = parts[0]; + final String saltHashBase64String = parts[1]; + + // The first 22 characters decode to a 16-byte value for the salt. + final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); + final byte[] salt = bcryptBase64.decode(saltHashBase64String.substring(0, 22).getBytes(StandardCharsets.ISO_8859_1)); + // The remaining characters are cipher text to be compared for authentication. + final String hashString = saltHashBase64String.substring(22).trim(); + final byte[] hash = bcryptBase64.decode(hashString.getBytes(StandardCharsets.ISO_8859_1)); + + return new BCryptHash(id, salt, hash, Integer.parseInt(costString, 10)); + } +} diff --git a/crypto/hash/src/main/resources/META-INF/NOTICE b/crypto/hash/src/main/resources/META-INF/NOTICE index 9d26a95ffb..29ce9b121c 100644 --- a/crypto/hash/src/main/resources/META-INF/NOTICE +++ b/crypto/hash/src/main/resources/META-INF/NOTICE @@ -7,7 +7,10 @@ The Apache Software Foundation (http://www.apache.org/). The implementation for org.apache.shiro.util.SoftHashMap is based on initial ideas from Dr. Heinz Kabutz's publicly posted version available at http://www.javaspecialists.eu/archive/Issue015.html, -with continued modifications. +with continued modifications. + +The implementation of Radix64 aka BcryptBase64 (OpenBSD BCrypt’s Base64) was copied +from https://github.com/patrickfav/bcrypt. Certain parts (StringUtils, IpAddressMatcher, etc.) of the source code for this product was copied for simplicity and to reduce diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy new file mode 100644 index 0000000000..54d8c9aead --- /dev/null +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy @@ -0,0 +1,59 @@ +/* + * 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.junit.jupiter.api.Test; + +import java.security.SecureRandom; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BCryptHashTest { + + private static final String TEST_PASSWORD = "secret#shiro,password;Jo8opech"; + + @Test + public void testCreateHashGenerateSaltIterations() { + // given + final char[] testPasswordChars = TEST_PASSWORD.toCharArray(); + + // when + final BCryptHash bCryptHash = BCryptHash.generate(testPasswordChars); + + // then + assertEquals(BCryptHash.DEFAULT_ITERATIONS, bCryptHash.getIterations()); + } + + @Test + public void testCreateHashGivenSalt() { + // given + final char[] testPasswordChars = TEST_PASSWORD.toCharArray(); + final byte[] salt = new SecureRandom().generateSeed(16); + + // when + final BCryptHash bCryptHash = BCryptHash.generate(testPasswordChars, salt, 6); + + // then + assertEquals(6, bCryptHash.getIterations()); + assertArrayEquals(salt, bCryptHash.getSalt().getBytes()); + } + +} 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 17ec82de5c..44287aa546 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 @@ -20,6 +20,7 @@ package org.apache.shiro.crypto.hash.format import org.apache.shiro.crypto.hash.Sha1Hash import org.junit.Test + import static org.junit.Assert.* /** @@ -82,6 +83,18 @@ class DefaultHashFormatFactoryTest { assertTrue instance instanceof Shiro1CryptFormat } + @Test + void testGetInstanceWithBcrypt() { + // given + def factory = new DefaultHashFormatFactory() + + // when + def instance = factory.getInstance('$unixcrypt$') + + // then + assertTrue instance instanceof UnixCryptFormat + } + @Test void testAbsentFQCN() { def factory = new DefaultHashFormatFactory() 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 21229d852a..3ad8d6aa84 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy @@ -19,6 +19,7 @@ package org.apache.shiro.crypto.hash.format import org.junit.Test + import static org.junit.Assert.* /** @@ -31,7 +32,7 @@ class ProvidedHashFormatTest { @Test void testDefaults() { def set = ProvidedHashFormat.values() as Set - assertEquals 3, set.size() + assertEquals 4, set.size() assertTrue set.contains(ProvidedHashFormat.HEX) assertTrue set.contains(ProvidedHashFormat.BASE64) assertTrue set.contains(ProvidedHashFormat.SHIRO1) diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy index 6ff0fdbe7f..b095fe39ec 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy @@ -18,16 +18,17 @@ */ package org.apache.shiro.crypto.hash.format -import org.apache.shiro.crypto.hash.Hash + +import org.apache.shiro.crypto.hash.SimpleHash /** * Simple {@code HashFormat} for testing that merely returns {@code hash.toString()}. * * @since 1.2 */ -class ToStringHashFormat implements HashFormat { +class ToStringHashFormat implements HashFormat { - String format(Hash hash) { + String format(SimpleHash hash) { return hash.toString() } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy new file mode 100644 index 0000000000..fd8723bc81 --- /dev/null +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy @@ -0,0 +1,108 @@ +/* + * 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.BCryptHash +import org.apache.shiro.crypto.hash.Hash +import org.apache.shiro.lang.codec.OpenBSDBase64 +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable + +import java.nio.charset.StandardCharsets + +import static org.junit.jupiter.api.Assertions.* + +class UnixCryptFormatTest { + + private static final byte[] PRECOMPUTED_SALT = [ + -10, -44, 37, -72, 40, 120, 88, 16, -116, 50, -54, -50, + -84, 66, -87, -14 + ]; + private static final byte[] PRECOMPUTED_HASHDATA = [ + -65, 4, 25, -47, 47, -68, -66, -66, 21, -89, -4, -15, + -16, 125, 25, -97, -123, 118, 73, -89, -71, 0, -128 + ]; + private static final int PRECOMPUTED_COST = 10; + + private final UnixCryptFormat bCryptFormat = new UnixCryptFormat(); + + @Test + void testFormatParseable() { + // given + // 'secret#shiro,password;Jo8opech' will work + final String formattedBcryptString = '$unixcrypt$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.'; + + // when + final Hash hash = this.bCryptFormat.parse(formattedBcryptString); + + // then + assertTrue(hash instanceof BCryptHash); + final BCryptHash bCryptHash = (BCryptHash) hash; + + assertAll( + { assertEquals("2y", hash.getAlgorithmName()) } as Executable, + { assertEquals(PRECOMPUTED_COST, hash.getIterations()) } as Executable, + { assertEquals(PRECOMPUTED_COST, bCryptHash.getCost()) } as Executable, + { assertEquals(1024, bCryptHash.getRealIterations()) } as Executable, + { + assertArrayEquals( + PRECOMPUTED_SALT, + hash.getSalt().getBytes() + ) + } as Executable, + { + assertArrayEquals( + PRECOMPUTED_HASHDATA, + hash.getBytes() + ) + } as Executable + ); + } + + @Test + void testFormatValidHash() { + // given + final Hash bcryptHash = new BCryptHash(PRECOMPUTED_SALT, PRECOMPUTED_HASHDATA, PRECOMPUTED_COST); + + + // when + final String formatted = this.bCryptFormat.format(bcryptHash); + + // then + assertTrue(formatted.startsWith('$unixcrypt$2y$10$')); + assertFalse(formatted.contains('$$')); + assertFalse(formatted.contains('=')); + } + + @Test + void testBase64EncodeDecode() { + // given + final String str = 'pWgo8H6sknPlLLSO2KkaLCarFwi8kF6'; + final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); + + // when + final byte[] decode = bcryptBase64.decode(str.getBytes(StandardCharsets.ISO_8859_1)); + final byte[] encode = bcryptBase64.encode(decode); + + // then + assertArrayEquals(str.getBytes(StandardCharsets.ISO_8859_1), encode); + } + +} diff --git a/lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java b/lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java new file mode 100644 index 0000000000..c6184e67af --- /dev/null +++ b/lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java @@ -0,0 +1,194 @@ +/* + * 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.lang.codec; + + +/** + * 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 + *

+ * According to Wikipedia: + * + *

+ * 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. + *
+ */ +public 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 "m0CrhHm10qJ3lXRY.5zDGO".getBytes(StandardCharsets.UTF8) + * @return the raw bytes encoded by this utf-8 radix4 string + */ + byte[] decode(byte[] utf8EncodedRadix64String); + + /* + * 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. + */ + + /** + * A mod of Square's Okio Base64 encoder + *

+ * Original author: Alexander Y. Kleymenov + * + * @see Okio + */ + 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/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java index 4ae65c91be..b748d32625 100644 --- a/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java +++ b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java @@ -322,6 +322,18 @@ public static String[] splitKeyValue(String aLine) throws ParseException { 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. + * + *

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}.

+ * + * @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); } From 137315f48ab9e527c69eb9ced22809fd32ffde87 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Sun, 3 Jan 2021 17:52:01 +0100 Subject: [PATCH 03/11] [SHIRO-290] Rework to use existing Shiro1CryptFormat. - Hashes can now compare themselves to a given password. Reviwers: Review method placement and HAsh class description. - removed hashrequest - removed UnixCryptFormat - API change: made salt not-nullable. Additional constructor is supplied for hashing without or with default salt, the former and other methods/fields using SimpleByteSource.empty(). Reviewers: Pay attention to method logic, so no empty salt is being used where a former `null` value would have created a new, random salt. - Modified tests to not expect exceptions in certain cases. - Modified tests to not expect passwordService calls when supplying an existing hash. - TBD: Fix Javadocs - TBD: Fix Hasher utility - TBD: Deprecate old non-KDF hash classes --- .../shiro/authc/SimpleAuthenticationInfo.java | 25 +++- .../credential/DefaultPasswordService.java | 22 +-- .../credential/HashedCredentialsMatcher.java | 20 +-- .../authc/credential/PasswordMatcher.java | 5 +- .../credential/PasswordMatcherTest.groovy | 15 +-- .../shiro/crypto/hash/AbstractHash.java | 11 +- .../apache/shiro/crypto/hash/BCryptHash.java | 40 ++++-- .../shiro/crypto/hash/DefaultHashService.java | 10 +- .../org/apache/shiro/crypto/hash/Hash.java | 1 + .../apache/shiro/crypto/hash/HashRequest.java | 3 +- .../apache/shiro/crypto/hash/SimpleHash.java | 74 +++++++--- .../shiro/crypto/hash/SimpleHashRequest.java | 11 +- .../crypto/hash/format/Base64Format.java | 10 +- .../hash/format/DefaultHashFormatFactory.java | 4 +- .../shiro/crypto/hash/format/HashFormat.java | 4 +- .../crypto/hash/format/HashFormatFactory.java | 2 +- .../shiro/crypto/hash/format/HexFormat.java | 6 +- .../hash/format/ModularCryptFormat.java | 4 +- .../hash/format/ParsableHashFormat.java | 2 +- .../hash/format/ProvidedHashFormat.java | 9 +- .../crypto/hash/format/Shiro1CryptFormat.java | 61 +++++++-- .../crypto/hash/format/UnixCryptFormat.java | 127 ------------------ .../hash/format/Base64FormatTest.groovy | 6 +- .../DefaultHashFormatFactoryTest.groovy | 12 -- .../hash/format/ProvidedHashFormatTest.groovy | 2 +- .../hash/format/Shiro1CryptFormatTest.groovy | 7 +- .../hash/format/ToStringHashFormat.groovy | 7 +- .../hash/format/UnixCryptFormatTest.groovy | 108 --------------- .../apache/shiro/lang/codec/CodecSupport.java | 40 +++--- .../shiro/lang/util/SimpleByteSource.java | 13 +- 30 files changed, 271 insertions(+), 390 deletions(-) delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java delete mode 100644 crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy 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 63d3cf5b8e..892b061b27 100644 --- a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java +++ b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java @@ -18,10 +18,11 @@ */ 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; @@ -37,6 +38,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 +53,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal * * @since 1.1 */ - protected ByteSource credentialsSalt; + protected ByteSource credentialsSalt = SimpleByteSource.empty(); /** * Default no-argument constructor. @@ -124,6 +126,7 @@ public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCre } + @Override public PrincipalCollection getPrincipals() { return principals; } @@ -137,6 +140,7 @@ public void setPrincipals(PrincipalCollection principals) { this.principals = principals; } + @Override public Object getCredentials() { return credentials; } @@ -163,6 +167,7 @@ public void setCredentials(Object credentials) { * hashed at all. * @since 1.1 */ + @Override public ByteSource getCredentialsSalt() { return credentialsSalt; } @@ -189,6 +194,7 @@ public void setCredentialsSalt(ByteSource salt) { * * @param info the AuthenticationInfo to add into this instance. */ + @Override @SuppressWarnings("unchecked") public void merge(AuthenticationInfo info) { if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) { @@ -249,14 +255,21 @@ public void merge(AuthenticationInfo info) { * @return true if the Object argument is an instanceof SimpleAuthenticationInfo and * its {@link #getPrincipals() principals} are equal to this instance's principals, false 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 (principals != null ? !principals.equals(that.principals) : that.principals != null) { + return false; + } return true; } @@ -266,6 +279,7 @@ public boolean equals(Object o) { * * @return the hashcode of the internal {@link #getPrincipals() principals} instance. */ + @Override public int hashCode() { return (principals != null ? principals.hashCode() : 0); } @@ -275,6 +289,7 @@ public int hashCode() { * * @return {@link #getPrincipals() principals}.toString() */ + @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 566376280c..08f11c3388 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 @@ -39,8 +39,7 @@ *

Hashing Passwords

* *

Comparing Passwords

- * 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 */ @@ -99,12 +98,7 @@ public boolean passwordsMatch(Object plaintext, Hash saved) { } } - HashRequest request = buildHashRequest(plaintextBytes, saved); - - // TODO: hashrequest for (b)crypt - final Hash computed = this.hashService.computeHash(request); - - return constantEquals(saved.toString(), computed.toString()); + return saved.matchesPassword(plaintextBytes); } private boolean constantEquals(String savedHash, String computedHash) { @@ -160,7 +154,7 @@ public boolean passwordsMatch(Object submittedPlaintext, String saved) { //configuration changes. HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved); - if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) { + if (discoveredFormat instanceof ParsableHashFormat) { ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat; Hash savedHash = parsableHashFormat.parse(saved); @@ -183,16 +177,6 @@ public boolean passwordsMatch(Object submittedPlaintext, String saved) { 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 1377374b23..53efb06929 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. @@ -341,6 +344,7 @@ protected Object getSalt(AuthenticationToken token) { * @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 +404,14 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo * @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()); } @@ -442,7 +446,7 @@ private String assertHashAlgorithmName() throws IllegalStateException { */ 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/PasswordMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java index e687dcc1a1..8e59441a04 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 @@ public PasswordMatcher() { this.passwordService = new DefaultPasswordService(); } + @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { PasswordService service = ensurePasswordService(); @@ -49,8 +51,7 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo 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; 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 85b75e7e02..2e2b0016ff 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 @@ -23,7 +23,7 @@ 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.UnixCryptFormat +import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat import org.junit.Test import static org.easymock.EasyMock.* @@ -90,11 +90,7 @@ class PasswordMatcherTest { matcher.passwordService = service assertSame service, matcher.passwordService - try { - assertTrue matcher.doCredentialsMatch(token, info) - fail "matcher should fail since PasswordService is not a HashingPasswordService" - } catch (IllegalStateException expected) { - } + assertTrue matcher.doCredentialsMatch(token, info) verify token, info, service } @@ -111,8 +107,6 @@ class PasswordMatcherTest { expect(token.credentials).andReturn submittedPassword expect(info.credentials).andReturn savedPassword - expect(service.passwordsMatch(submittedPassword, savedPassword)).andReturn true - replay token, info, service def matcher = new PasswordMatcher() @@ -184,8 +178,8 @@ class PasswordMatcherTest { void testBCryptPassword() { // given def matcher = new PasswordMatcher(); - def bcryptPw = '$unixcrypt$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' - def bcryptHash = new UnixCryptFormat().parse(bcryptPw); + def bcryptPw = '$shiro1$2y$10$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + def bcryptHash = new Shiro1CryptFormat().parse(bcryptPw); def plaintext = 'secret#shiro,password;Jo8opech' def principal = "user" def usernamePasswordToken = new UsernamePasswordToken(principal, plaintext) @@ -194,6 +188,7 @@ class PasswordMatcherTest { // when def match = matcher.doCredentialsMatch(usernamePasswordToken, authenticationInfo) + // then assertTrue match } 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 4bf8373ae0..10e071179c 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 @@ public AbstractHash(Object source, Object salt, int hashIterations) throws Codec * * @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 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn * * @return a hex-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toHex() { if (this.hexEncoded == null) { this.hexEncoded = Hex.encodeToString(getBytes()); @@ -249,6 +253,7 @@ public String toHex() { * * @return a Base64-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toBase64() { if (this.base64Encoded == null) { //cache result in case this method is called multiple times. @@ -262,6 +267,7 @@ public String toBase64() { * * @return the {@link #toHex() toHex()} value. */ + @Override public String toString() { return toHex(); } @@ -274,6 +280,7 @@ public String toString() { * @return {@code true} if the specified object is a Hash and its {@link #getBytes byte array} is identical to * this Hash's byte array, {@code false} otherwise. */ + @Override public boolean equals(Object o) { if (o instanceof Hash) { Hash other = (Hash) o; @@ -287,6 +294,7 @@ public boolean equals(Object o) { * * @return toHex().hashCode() */ + @Override public int hashCode() { if (this.bytes == null || this.bytes.length == 0) { return 0; @@ -307,6 +315,7 @@ private static boolean isReserved(String arg) { return "-base64".equals(arg) || "-times".equals(arg) || "-salt".equals(arg); } + @Deprecated static int doMain(Class clazz, String[] args) { String simple = clazz.getSimpleName(); int index = simple.indexOf("Hash"); diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java index da2098541f..8f30e7571f 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -28,6 +28,7 @@ import java.security.SecureRandom; import java.util.Arrays; import java.util.StringJoiner; +import java.util.regex.Pattern; public class BCryptHash extends AbstractCryptHash { @@ -39,21 +40,23 @@ public class BCryptHash extends AbstractCryptHash { private static final int SALT_LENGTH = 16; + private static final Pattern DELIMITER = Pattern.compile("\\$"); + private final String version; - private final byte[] salt; + private final ByteSource salt; private final byte[] hashedData; private final int cost; - public BCryptHash(final byte[] salt, final byte[] hashedData, final int cost) { - this(ALGORITHM_NAME, salt, hashedData, cost); + public BCryptHash(final byte[] hashedData, final ByteSource salt, final int cost) { + this(ALGORITHM_NAME, hashedData, salt, cost); } - public BCryptHash(final String version, final byte[] salt, final byte[] hashedData, final int cost) { + public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) { super(); this.version = version; + this.hashedData = Arrays.copyOf(hashedData, hashedData.length); this.salt = salt; - this.hashedData = hashedData; this.cost = cost; } @@ -65,14 +68,26 @@ public static BCryptHash generate(final char[] source) { public static BCryptHash generate(final char[] source, final byte[] initialSalt, final int cost) { final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, source, initialSalt, cost); - final String dataSection = cryptString.substring(cryptString.lastIndexOf('$') + 1); + return fromCryptString(cryptString); + } + + private static BCryptHash fromCryptString(String cryptString) { + String[] parts = DELIMITER.split(cryptString.substring(1), -1); + + if (parts.length != 3) { + throw new IllegalArgumentException("Expected string containing three '$' but got: '" + Arrays.toString(parts) + "'."); + } + + final String algorithmName = parts[0]; + final int cost = Integer.parseInt(parts[1], 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(ALGORITHM_NAME, salt, hashedData, cost); + return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), cost); } protected static byte[] createSalt() { @@ -91,7 +106,7 @@ public int getSaltLength() { @Override public ByteSource getSalt() { - return new SimpleByteSource(Arrays.copyOf(this.salt, SALT_LENGTH)); + return this.salt; } /** @@ -104,6 +119,13 @@ public int getIterations() { return this.getCost(); } + @Override + public boolean matchesPassword(ByteSource plaintextBytes) { + final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, plaintextBytes.getBytes(), this.salt.getBytes(), cost); + + return this.equals(fromCryptString(cryptString)); + } + public int getRealIterations() { return (int) Math.pow(2, this.getCost()); } @@ -127,7 +149,7 @@ public String toString() { return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]") .add("super=" + super.toString()) .add("version='" + this.version + "'") - .add("salt=" + Arrays.toString(this.salt)) + .add("salt=" + Arrays.toString(this.salt.getBytes())) .add("hashedData=" + Arrays.toString(this.hashedData)) .add("cost=" + this.cost) .toString(); 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 486e19df79..c082ca2eb2 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 @@ -21,6 +21,7 @@ import org.apache.shiro.crypto.RandomNumberGenerator; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; /** * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name, @@ -81,7 +82,7 @@ public class DefaultHashService implements ConfigurableHashService { /** * The 'private' part of the hash salt. */ - private ByteSource privateSalt; + private ByteSource privateSalt = SimpleByteSource.empty(); /** * The number of hash iterations to perform when computing hashes. @@ -147,6 +148,7 @@ public DefaultHashService() { * @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; @@ -254,7 +256,7 @@ protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { int length = privateSaltLength + extraBytesLength; if (length <= 0) { - return null; + return SimpleByteSource.empty(); } byte[] combined = new byte[length]; @@ -272,6 +274,7 @@ protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { return ByteSource.Util.bytes(combined); } + @Override public void setHashAlgorithmName(String name) { this.algorithmName = name; } @@ -280,6 +283,7 @@ public String getHashAlgorithmName() { return this.algorithmName; } + @Override public void setPrivateSalt(ByteSource privateSalt) { this.privateSalt = privateSalt; } @@ -288,6 +292,7 @@ public ByteSource getPrivateSalt() { return this.privateSalt; } + @Override public void setHashIterations(int count) { this.iterations = count; } @@ -296,6 +301,7 @@ public int getHashIterations() { return this.iterations; } + @Override public void setRandomNumberGenerator(RandomNumberGenerator rng) { this.rng = rng; } 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 3e26928601..b0a9650fd2 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 @@ -64,4 +64,5 @@ public interface Hash extends ByteSource { */ int getIterations(); + boolean matchesPassword(ByteSource plaintextBytes); } 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 79d32514f3..b66c79ac86 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,7 @@ package org.apache.shiro.crypto.hash; import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; /** * A {@code HashRequest} is composed of data that will be used by a {@link HashService} to compute a hash (aka @@ -85,7 +86,7 @@ public interface HashRequest { public static class Builder { private ByteSource source; - private ByteSource salt; + private ByteSource salt = SimpleByteSource.empty(); private int iterations; private String algorithmName; 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 8c1fb6e939..c8b8e7ef89 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,20 @@ */ 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 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 +46,7 @@ public class SimpleHash extends AbstractHash { private static final int DEFAULT_ITERATIONS = 1; + private static final long serialVersionUID = -6689895264902387303L; /** * The {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash. @@ -114,7 +118,7 @@ public SimpleHash(String algorithmName) { */ public SimpleHash(String algorithmName, Object source) throws CodecException, UnknownAlgorithmException { //noinspection NullableProblems - this(algorithmName, source, null, DEFAULT_ITERATIONS); + this(algorithmName, source, SimpleByteSource.empty(), DEFAULT_ITERATIONS); } /** @@ -139,6 +143,28 @@ public SimpleHash(String algorithmName, Object source, Object salt) throws Codec this(algorithmName, source, salt, DEFAULT_ITERATIONS); } + /** + * Creates an {@code algorithmName}-specific hash of the specified {@code source} using the given {@code salt} + * using a single hash iteration. + *

+ * It is a convenience constructor that merely executes this( algorithmName, source, salt, 1);. + *

+ * 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. @@ -169,11 +195,8 @@ public SimpleHash(String algorithmName, Object source, Object salt, int hashIter } 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 +232,20 @@ protected ByteSource convertSaltToBytes(Object salt) { /** * 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 +255,27 @@ private void hash(ByteSource source, ByteSource salt, int hashIterations) throws * * @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) { + return this.equals(new SimpleHash(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations())); + } + + @Override public byte[] getBytes() { return this.bytes; } @@ -259,6 +288,7 @@ public byte[] getBytes() { * * @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 +328,7 @@ public void setSalt(ByteSource salt) { * @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 +345,7 @@ protected MessageDigest getDigest(String algorithmName) throws UnknownAlgorithmE * @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 +358,7 @@ protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException { * @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 +372,10 @@ protected byte[] hash(byte[] bytes, byte[] salt) throws UnknownAlgorithmExceptio * @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 +389,7 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn return hashed; } + @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } @@ -368,6 +403,7 @@ public boolean isEmpty() { * * @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 +420,7 @@ public String toHex() { * * @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 +434,7 @@ public String toBase64() { * * @return the {@link #toHex() toHex()} value. */ + @Override public String toString() { return toHex(); } @@ -409,6 +447,7 @@ public String toString() { * @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 +461,7 @@ public boolean equals(Object o) { * * @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/SimpleHashRequest.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java index 5423256739..5c3101ae58 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,8 @@ import org.apache.shiro.lang.util.ByteSource; +import static java.util.Objects.requireNonNull; + /** * Simple implementation of {@link HashRequest} that can be used when interacting with a {@link HashService}. * @@ -47,27 +49,28 @@ public class SimpleHashRequest implements HashRequest { * @throws NullPointerException if {@code source} is null or empty. */ public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, int iterations) { - if (source == null) { - throw new NullPointerException("source argument cannot be null"); - } - this.source = source; + this.source = requireNonNull(source); this.salt = salt; this.algorithmName = algorithmName; this.iterations = Math.max(0, iterations); } + @Override public ByteSource getSource() { return this.source; } + @Override public ByteSource getSalt() { return this.salt; } + @Override public int getIterations() { return iterations; } + @Override public String getAlgorithmName() { return algorithmName; } 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 94088acbe7..022c3067ef 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 @@ -18,7 +18,9 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.crypto.hash.Hash; + +import static java.util.Objects.requireNonNull; /** * {@code HashFormat} that outputs only the hash's digest bytes in Base64 format. It does not print out @@ -27,7 +29,7 @@ * * @since 1.2 */ -public class Base64Format implements HashFormat { +public class Base64Format implements HashFormat { /** * Returns {@code hash != null ? hash.toBase64() : null}. @@ -36,7 +38,7 @@ public class Base64Format implements HashFormat { * @return {@code hash != null ? hash.toBase64() : null}. */ @Override - public String format(final SimpleHash hash) { - return hash != null ? hash.toBase64() : null; + 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 02f080555d..ae09b13bce 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 @@ -112,12 +112,12 @@ public void setSearchPackages(Set searchPackages) { } @Override - public HashFormat getInstance(String in) { + public HashFormat getInstance(String in) { if (in == null) { return null; } - HashFormat hashFormat = null; + HashFormat hashFormat = null; Class clazz = null; //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance 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 700e2f071b..c65ae78b5f 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 @@ -33,7 +33,7 @@ * * @since 1.2 */ -public interface HashFormat { +public interface HashFormat { /** * Returns a formatted string representing the specified Hash instance. @@ -41,5 +41,5 @@ public interface HashFormat { * @param hash the hash instance to format into a String. * @return a formatted string representing the specified Hash instance. */ - String format(T hash); + String format(Hash hash); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java index 5cc6b2583d..fa52691098 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java @@ -23,5 +23,5 @@ */ public interface HashFormatFactory { - HashFormat getInstance(String token); + HashFormat getInstance(String token); } 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 8126a0bb23..ad30293434 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 @@ -18,7 +18,7 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.crypto.hash.Hash; /** * {@code HashFormat} that outputs only The hash's digest bytes in hex format. It does not print out @@ -27,7 +27,7 @@ * * @since 1.2 */ -public class HexFormat implements HashFormat { +public class HexFormat implements HashFormat { /** * Returns {@code hash != null ? hash.toHex() : null}. @@ -36,7 +36,7 @@ public class HexFormat implements HashFormat { * @return {@code hash != null ? hash.toHex() : null}. */ @Override - public String format(final SimpleHash hash) { + public String format(final Hash hash) { return hash != null ? hash.toHex() : null; } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java index 10847d9858..ce4917556e 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java @@ -18,8 +18,6 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.Hash; - /** * A {@code HashFormat} that supports * Modular Crypt Format token rules. @@ -28,7 +26,7 @@ * @see MCF Journal Entry * @since 1.2 */ -public interface ModularCryptFormat extends HashFormat { +public interface ModularCryptFormat extends HashFormat { public static final String TOKEN_DELIMITER = "$"; diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java index af3a0de930..045756805b 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java @@ -31,7 +31,7 @@ * * @since 1.2 */ -public interface ParsableHashFormat extends HashFormat { +public interface ParsableHashFormat extends HashFormat { /** * Parses the specified formatted string and returns the corresponding Hash instance. 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 16f559ea61..6e22a728a4 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,16 +40,11 @@ public enum ProvidedHashFormat { /** * Value representing the {@link Shiro1CryptFormat} implementation. */ - SHIRO1(Shiro1CryptFormat.class), - - /** - * Unix Crypt format {@link UnixCryptFormat} as they are used in {@code /etc/shadow} files. - */ - UNIXCRYPT(UnixCryptFormat.class); + SHIRO1(Shiro1CryptFormat.class); private final Class clazz; - private ProvidedHashFormat(final Class clazz) { + ProvidedHashFormat(final Class clazz) { this.clazz = clazz; } 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 deface27fd..52a6ecb9e2 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,12 +18,19 @@ */ package org.apache.shiro.crypto.hash.format; +import org.apache.shiro.crypto.hash.BCryptHash; 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.OpenBSDBase64; import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; import org.apache.shiro.lang.util.StringUtils; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + /** * The {@code Shiro1CryptFormat} is a fully reversible * Modular Crypt Format (MCF). Because it is @@ -85,10 +92,11 @@ * * @since 1.2 */ -public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat { +public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat { public static final String ID = "shiro1"; public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER; + private static final List ALGORITHMS_BCRYPT = Arrays.asList("2", "2a", "2b", "2y"); public Shiro1CryptFormat() { } @@ -99,7 +107,7 @@ public String getId() { } @Override - public String format(final SimpleHash hash) { + public String format(final Hash hash) { if (hash == null) { return null; } @@ -141,13 +149,13 @@ public Hash parse(final String formatted) { final String iterationsString = parts[i--]; final String algorithmName = parts[i]; - final byte[] digest = Base64.decode(digestBase64); - ByteSource salt = null; - - if (StringUtils.hasLength(saltBase64)) { - final byte[] saltBytes = Base64.decode(saltBase64); - salt = ByteSource.Util.bytes(saltBytes); + final byte[] digest; + if (ALGORITHMS_BCRYPT.contains(algorithmName)) { + digest = new OpenBSDBase64.Default().decode(digestBase64.getBytes(StandardCharsets.ISO_8859_1)); + } else { + digest = Base64.decode(digestBase64); } + ByteSource salt = parseSalt(saltBase64, algorithmName); final int iterations; try { @@ -157,13 +165,38 @@ public Hash parse(final String formatted) { throw new IllegalArgumentException(msg, e); } - final SimpleHash hash = new SimpleHash(algorithmName); - hash.setBytes(digest); - if (salt != null) { - hash.setSalt(salt); + switch (algorithmName) { + case "2": + case "2a": + case "2b": + case "2y": + return new BCryptHash(algorithmName, digest, salt, iterations); + default: + final SimpleHash hash = new SimpleHash(algorithmName); + hash.setBytes(digest); + hash.setSalt(salt); + hash.setIterations(iterations); + + return hash; + } + } + + private ByteSource parseSalt(String base64, String algorithmName) { + if (!StringUtils.hasLength(base64)) { + return SimpleByteSource.empty(); } - hash.setIterations(iterations); - return hash; + switch (algorithmName) { + case "2": + case "2a": + case "2b": + case "2y": + byte[] saltBytesBcrypt = new OpenBSDBase64.Default().decode(base64.getBytes(StandardCharsets.ISO_8859_1)); + return new SimpleByteSource(saltBytesBcrypt); + default: + final byte[] saltBytes = Base64.decode(base64); + return new SimpleByteSource(saltBytes); + + } } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java deleted file mode 100644 index 66e3202609..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/UnixCryptFormat.java +++ /dev/null @@ -1,127 +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.format; - -import org.apache.shiro.crypto.hash.BCryptHash; -import org.apache.shiro.crypto.hash.Hash; -import org.apache.shiro.lang.codec.OpenBSDBase64; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static java.util.Objects.requireNonNull; - -public class UnixCryptFormat implements ModularCryptFormat, ParsableHashFormat { - - private static final String ID = "unixcrypt"; - private static final List BCRYPT_IDS = Arrays.asList("2", "2a", "2b", "2y"); - private static final List BCRYPT_PREFIXES = createPrefixes(); - - private static final String DELIMITER_CHAR = "$"; - private static final Pattern DELIMITER = Pattern.compile("\\" + DELIMITER_CHAR); - - private static List createPrefixes() { - final List prefixes = BCRYPT_IDS.stream() - .map(id -> TOKEN_DELIMITER + id + TOKEN_DELIMITER) - .collect(Collectors.toList()); - - return Collections.unmodifiableList(prefixes); - } - - @Override - public String getId() { - return ID; - } - - - @Override - public String format(final BCryptHash hash) { - return "$" + ID + this.getCanonicalFormat(hash); - - } - - protected String getCanonicalFormat(final Hash hash) { - final String algorithmName = requireNonNull(hash).getAlgorithmName(); - - if (!BCRYPT_IDS.contains(algorithmName)) { - final String msg = "AlgorithmName [" + algorithmName + "] is not a valid bcrypt algorithm." - + "Allowed values: [" + BCRYPT_IDS + "]."; - throw new IllegalArgumentException(msg); - } - - final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); - final String saltHex = new String(bcryptBase64.encode(hash.getSalt().getBytes()), StandardCharsets.ISO_8859_1); - final byte[] bytes = hash.getBytes(); - final String byteHex = new String(bcryptBase64.encode(bytes), StandardCharsets.ISO_8859_1) - .replace('+', '.'); - - return new StringJoiner(DELIMITER_CHAR, DELIMITER_CHAR, "") - .add(algorithmName) - .add("" + hash.getIterations()) - .add(saltHex + byteHex) - .toString(); - } - - @Override - public Hash parse(final String formatted) { - requireNonNull(formatted); - if (!formatted.startsWith("$" + ID + "$")) { - final String msg = "The argument is not a valid bcrypt formatted hash. " - + "Expected to start with $" + ID + "$ but was " + formatted + "."; - throw new IllegalArgumentException(msg); - } - - final String canonicalFormat = formatted.substring(formatted.indexOf("$", 1)); - - final Optional matchedPrefix = BCRYPT_PREFIXES.stream() - .filter(canonicalFormat::startsWith) - .findAny(); - if (!matchedPrefix.isPresent()) { - final String msg = "The argument is not a valid bcrypt formatted hash. " - + "Expected canconical form to start with any of " + BCRYPT_IDS - + ", but found " + canonicalFormat + "."; - throw new IllegalArgumentException(msg); - } - - final String prefix = matchedPrefix.orElseThrow(NoSuchElementException::new); - final String id = prefix.substring(1, prefix.length() - 1); - final String suffix = canonicalFormat.substring(canonicalFormat.indexOf(DELIMITER_CHAR, 1) + 1); - final String[] parts = DELIMITER.split(suffix); - - final String costString = parts[0]; - final String saltHashBase64String = parts[1]; - - // The first 22 characters decode to a 16-byte value for the salt. - final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); - final byte[] salt = bcryptBase64.decode(saltHashBase64String.substring(0, 22).getBytes(StandardCharsets.ISO_8859_1)); - // The remaining characters are cipher text to be compared for authentication. - final String hashString = saltHashBase64String.substring(22).trim(); - final byte[] hash = bcryptBase64.decode(hashString.getBytes(StandardCharsets.ISO_8859_1)); - - return new BCryptHash(id, salt, hash, Integer.parseInt(costString, 10)); - } -} 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 75cb26622d..11eaa732ca 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 @@ -21,7 +21,9 @@ 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 static org.junit.Assert.assertEquals +import static org.junit.Assert.assertThrows /** * Unit tests for the {@link Base64Format} implementation. @@ -41,7 +43,7 @@ class Base64FormatTest { @Test void testFormatWithNullArgument() { Base64Format format = new Base64Format() - assertNull format.format(null) + assertThrows NullPointerException.class, { format.format(null) } } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy index 44287aa546..daa26e8371 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 @@ -83,18 +83,6 @@ class DefaultHashFormatFactoryTest { assertTrue instance instanceof Shiro1CryptFormat } - @Test - void testGetInstanceWithBcrypt() { - // given - def factory = new DefaultHashFormatFactory() - - // when - def instance = factory.getInstance('$unixcrypt$') - - // then - assertTrue instance instanceof UnixCryptFormat - } - @Test void testAbsentFQCN() { def factory = new DefaultHashFormatFactory() 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 3ad8d6aa84..4d522faad5 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 @@ -32,7 +32,7 @@ class ProvidedHashFormatTest { @Test void testDefaults() { def set = ProvidedHashFormat.values() as Set - assertEquals 4, set.size() + assertEquals 3, 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 2b10c099f1..b4b38aab52 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy @@ -21,6 +21,7 @@ package org.apache.shiro.crypto.hash.format import org.apache.shiro.crypto.SecureRandomNumberGenerator import org.apache.shiro.crypto.hash.SimpleHash import org.junit.Test + import static org.junit.Assert.* /** @@ -65,7 +66,7 @@ class Shiro1CryptFormatTest { def rng = new SecureRandomNumberGenerator() def source = rng.nextBytes() - def hash = new SimpleHash(alg, source, null, iterations) + def hash = new SimpleHash(alg, source, iterations) String formatted = format.format(hash); @@ -120,7 +121,7 @@ class Shiro1CryptFormatTest { def rng = new SecureRandomNumberGenerator() def source = rng.nextBytes() - def hash = new SimpleHash(alg, source, null, iterations) + def hash = new SimpleHash(alg, source, iterations) String formatted = Shiro1CryptFormat.MCF_PREFIX + alg + delim + @@ -133,7 +134,7 @@ class Shiro1CryptFormatTest { assertEquals hash, parsedHash assertEquals hash.algorithmName, parsedHash.algorithmName assertEquals hash.iterations, parsedHash.iterations - assertNull hash.salt + assertTrue hash.salt.isEmpty() assertTrue Arrays.equals(hash.bytes, parsedHash.bytes) } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy index b095fe39ec..6ff0fdbe7f 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ToStringHashFormat.groovy @@ -18,17 +18,16 @@ */ package org.apache.shiro.crypto.hash.format - -import org.apache.shiro.crypto.hash.SimpleHash +import org.apache.shiro.crypto.hash.Hash /** * Simple {@code HashFormat} for testing that merely returns {@code hash.toString()}. * * @since 1.2 */ -class ToStringHashFormat implements HashFormat { +class ToStringHashFormat implements HashFormat { - String format(SimpleHash hash) { + String format(Hash hash) { return hash.toString() } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy deleted file mode 100644 index fd8723bc81..0000000000 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/UnixCryptFormatTest.groovy +++ /dev/null @@ -1,108 +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.format - -import org.apache.shiro.crypto.hash.BCryptHash -import org.apache.shiro.crypto.hash.Hash -import org.apache.shiro.lang.codec.OpenBSDBase64 -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.function.Executable - -import java.nio.charset.StandardCharsets - -import static org.junit.jupiter.api.Assertions.* - -class UnixCryptFormatTest { - - private static final byte[] PRECOMPUTED_SALT = [ - -10, -44, 37, -72, 40, 120, 88, 16, -116, 50, -54, -50, - -84, 66, -87, -14 - ]; - private static final byte[] PRECOMPUTED_HASHDATA = [ - -65, 4, 25, -47, 47, -68, -66, -66, 21, -89, -4, -15, - -16, 125, 25, -97, -123, 118, 73, -89, -71, 0, -128 - ]; - private static final int PRECOMPUTED_COST = 10; - - private final UnixCryptFormat bCryptFormat = new UnixCryptFormat(); - - @Test - void testFormatParseable() { - // given - // 'secret#shiro,password;Jo8opech' will work - final String formattedBcryptString = '$unixcrypt$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.'; - - // when - final Hash hash = this.bCryptFormat.parse(formattedBcryptString); - - // then - assertTrue(hash instanceof BCryptHash); - final BCryptHash bCryptHash = (BCryptHash) hash; - - assertAll( - { assertEquals("2y", hash.getAlgorithmName()) } as Executable, - { assertEquals(PRECOMPUTED_COST, hash.getIterations()) } as Executable, - { assertEquals(PRECOMPUTED_COST, bCryptHash.getCost()) } as Executable, - { assertEquals(1024, bCryptHash.getRealIterations()) } as Executable, - { - assertArrayEquals( - PRECOMPUTED_SALT, - hash.getSalt().getBytes() - ) - } as Executable, - { - assertArrayEquals( - PRECOMPUTED_HASHDATA, - hash.getBytes() - ) - } as Executable - ); - } - - @Test - void testFormatValidHash() { - // given - final Hash bcryptHash = new BCryptHash(PRECOMPUTED_SALT, PRECOMPUTED_HASHDATA, PRECOMPUTED_COST); - - - // when - final String formatted = this.bCryptFormat.format(bcryptHash); - - // then - assertTrue(formatted.startsWith('$unixcrypt$2y$10$')); - assertFalse(formatted.contains('$$')); - assertFalse(formatted.contains('=')); - } - - @Test - void testBase64EncodeDecode() { - // given - final String str = 'pWgo8H6sknPlLLSO2KkaLCarFwi8kF6'; - final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); - - // when - final byte[] decode = bcryptBase64.decode(str.getBytes(StandardCharsets.ISO_8859_1)); - final byte[] encode = bcryptBase64.encode(decode); - - // then - assertArrayEquals(str.getBytes(StandardCharsets.ISO_8859_1), encode); - } - -} 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 e503f7e2e4..d7fd0c8837 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 @@ protected boolean isByteSource(Object o) { * 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 18594f6f01..dbb8d3d3c1 100644 --- a/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java +++ b/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java @@ -130,14 +130,21 @@ public static boolean isCompatible(Object o) { o instanceof ByteSource || o instanceof File || o instanceof InputStream; } + public static ByteSource empty() { + return new SimpleByteSource(new byte[]{}); + } + + @Override public byte[] getBytes() { - return this.bytes; + return Arrays.copyOf(this.bytes, this.bytes.length); } + @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } + @Override public String toHex() { if ( this.cachedHex == null ) { this.cachedHex = Hex.encodeToString(getBytes()); @@ -145,6 +152,7 @@ public String toHex() { return this.cachedHex; } + @Override public String toBase64() { if ( this.cachedBase64 == null ) { this.cachedBase64 = Base64.encodeToString(getBytes()); @@ -152,10 +160,12 @@ public String toBase64() { return this.cachedBase64; } + @Override public String toString() { return toBase64(); } + @Override public int hashCode() { if (this.bytes == null || this.bytes.length == 0) { return 0; @@ -163,6 +173,7 @@ public int hashCode() { return Arrays.hashCode(this.bytes); } + @Override public boolean equals(Object o) { if (o == this) { return true; From 77badaa0a06672faf3687bd6189d6b081e634fe5 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Sun, 3 Jan 2021 22:40:42 +0100 Subject: [PATCH 04/11] [SHIRO-290] Prepare argon2 implementation. - BCrypt iterations vs cost: make iterations return iterations - add validate methods --- .../credential/PasswordMatcherTest.groovy | 4 +- .../shiro/crypto/hash/AbstractCryptHash.java | 104 ++++++++++++-- .../apache/shiro/crypto/hash/Argon2Hash.java | 136 ++++++++++++++++++ .../apache/shiro/crypto/hash/BCryptHash.java | 115 ++++++++------- .../crypto/hash/format/Shiro1CryptFormat.java | 6 +- .../shiro/crypto/hash/BCryptHashTest.groovy | 35 ++--- 6 files changed, 316 insertions(+), 84 deletions(-) create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java 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 2e2b0016ff..7edf1c0d47 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 @@ -26,6 +26,7 @@ import org.apache.shiro.crypto.hash.Sha256Hash import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat import org.junit.Test +import static java.lang.Math.pow import static org.easymock.EasyMock.* import static org.junit.Assert.* @@ -178,7 +179,8 @@ class PasswordMatcherTest { void testBCryptPassword() { // given def matcher = new PasswordMatcher(); - def bcryptPw = '$shiro1$2y$10$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + def iterations = (int) pow(2, 10) + def bcryptPw = '$shiro1$2y$' + iterations + '$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' def bcryptHash = new Shiro1CryptFormat().parse(bcryptPw); def plaintext = 'secret#shiro,password;Jo8opech' def principal = "user" 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 index 5961f1562a..c52c25babb 100644 --- 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 @@ -22,14 +22,23 @@ import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.lang.codec.CodecSupport; import org.apache.shiro.lang.codec.Hex; +import org.apache.shiro.lang.util.ByteSource; import java.io.Serializable; import java.security.MessageDigest; import java.util.Arrays; +import java.util.Locale; +import java.util.StringJoiner; + +import static java.util.Objects.requireNonNull; public abstract class AbstractCryptHash extends CodecSupport implements Hash, Serializable { private static final long serialVersionUID = 2483214646921027859L; + private final String version; + private final byte[] hashedData; + private final ByteSource salt; + private final int iterations; /** * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead. */ @@ -39,17 +48,84 @@ public abstract class AbstractCryptHash extends CodecSupport implements Hash, Se */ private String base64Encoded; + public AbstractCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) { + this.version = version; + this.hashedData = Arrays.copyOf(hashedData, hashedData.length); + this.salt = requireNonNull(salt); + this.iterations = cost; + checkValid(); + } + + protected final void checkValid() { + checkValidAlgorithm(); + + checkValidSalt(); + + checkValidIterations(); + } + + protected abstract void checkValidAlgorithm(); + + private 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); + } + } + + protected abstract void checkValidIterations(); + /** * Implemented by subclasses, this specifies the {@link MessageDigest MessageDigest} algorithm name * to use when performing the hash. * + *

When multiple algorithm names are acceptable, then this method should return the primary algorithm name.

+ * + *

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.

+ * * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash. */ @Override public abstract String getAlgorithmName(); + /** + * 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; + } + + /** + * Warning! The returned value is actually the cost, not the iterations. + * + * @return the cost. + */ + @Override + public int getIterations() { + return this.iterations; + } + + @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 #getBytes byte array}. @@ -82,16 +158,6 @@ public String toBase64() { return this.base64Encoded; } - /** - * Simple implementation that merely returns {@link #toHex() toHex()}. - * - * @return the {@link #toHex() toHex()} value. - */ - @Override - public String toString() { - return this.toHex(); - } - /** * Returns {@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. @@ -121,4 +187,22 @@ public int hashCode() { } return Arrays.hashCode(this.getBytes()); } + + /** + * 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("version='" + version + "'") + .add("hashedData=" + Arrays.toString(hashedData)) + .add("salt=" + salt) + .add("iterations=" + iterations) + .add("hexEncoded='" + hexEncoded + "'") + .add("base64Encoded='" + base64Encoded + "'") + .toString(); + } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java new file mode 100644 index 0000000000..4d87774da3 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java @@ -0,0 +1,136 @@ +/* + * 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.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.util.Collections.unmodifiableList; + +public class Argon2Hash extends AbstractCryptHash { + private static final long serialVersionUID = 2647354947284558921L; + + private static final String ALGORITHM_NAME = "argon2id"; + + private static final List ALGORITHMS_ARGON2 = Arrays.asList("argon2id", "argon2i", "argon2d"); + + private static final int DEFAULT_ITERATIONS = 10; + + private static final int DEFAULT_MEMORY = 1_048_576; + + private static final int DEFAULT_PARALLELISM = 4; + + /** + * 128 bits of salt is the recommended salt length. + */ + private static final int SALT_LENGTH = 16; + + public Argon2Hash(byte[] hashedData, ByteSource salt, int cost) { + super(ALGORITHM_NAME, hashedData, salt, cost); + } + + public Argon2Hash(String version, byte[] hashedData, ByteSource salt, int cost) { + super(version, hashedData, salt, cost); + } + + public static List getAlgorithmsArgon2() { + return unmodifiableList(ALGORITHMS_ARGON2); + } + + public static Argon2Hash generate(final char[] source) { + return generate(source, createSalt(), DEFAULT_ITERATIONS); + } + + public static byte[] createSalt() { + return new SecureRandom().generateSeed(SALT_LENGTH); + } + + + public static Argon2Hash generate(final char[] source, final byte[] salt, final int iterations) { + final Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) + .withVersion(Argon2Parameters.ARGON2_VERSION_13) + .withIterations(iterations) + .withMemoryAsKB(DEFAULT_MEMORY) + .withParallelism(DEFAULT_PARALLELISM) + .withSalt(salt) + .build(); + + final Argon2BytesGenerator gen = new Argon2BytesGenerator(); + gen.init(parameters); + + final byte[] hash = new byte[32]; + gen.generateBytes(source, hash); + + return new Argon2Hash(ALGORITHM_NAME, hash, new SimpleByteSource(salt), iterations); + } + + @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); + } + } + + @Override + 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 String getAlgorithmName() { + // TODO: implement + throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::getAlgorithmName]."); + } + + @Override + public boolean matchesPassword(ByteSource plaintextBytes) { + // TODO: implement + throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::matchesPassword]."); + } + + @Override + public int getSaltLength() { + // TODO: implement + throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::getSaltLength]."); + } + +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java index 8f30e7571f..7a2a78eeff 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -27,14 +27,18 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.StringJoiner; import java.util.regex.Pattern; +import static java.util.Collections.unmodifiableList; + public class BCryptHash extends AbstractCryptHash { private static final long serialVersionUID = 6957869292324606101L; - protected static final int DEFAULT_ITERATIONS = 10; + protected static final int DEFAULT_COST = 10; private static final String ALGORITHM_NAME = "2y"; @@ -42,26 +46,66 @@ public class BCryptHash extends AbstractCryptHash { private static final Pattern DELIMITER = Pattern.compile("\\$"); - private final String version; - private final ByteSource salt; - private final byte[] hashedData; - private final int cost; + private static final List ALGORITHMS_BCRYPT = Arrays.asList("2", "2a", "2b", "2y"); + public BCryptHash(final byte[] hashedData, final ByteSource salt, final int iterations) { + this(ALGORITHM_NAME, hashedData, salt, iterations); + } - public BCryptHash(final byte[] hashedData, final ByteSource salt, final int cost) { - this(ALGORITHM_NAME, hashedData, salt, cost); + public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int iterations) { + super(version, hashedData, salt, iterations); + } + + @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); + } } - public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) { - super(); - this.version = version; - this.hashedData = Arrays.copyOf(hashedData, hashedData.length); - this.salt = salt; - this.cost = cost; + @Override + protected final void checkValidIterations() { + double costDbl = Math.log10(this.getIterations()) / Math.log10(2); + if ((costDbl != Math.floor(costDbl)) || Double.isInfinite(costDbl)) { + throw new IllegalArgumentException("Iterations are not a power of 2. Found: [" + this.getIterations() + "]."); + } + + int cost = (int) costDbl; + 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); + } + + double iterations = Math.pow(2, cost); + if (iterations != getIterations()) { + throw new IllegalArgumentException("Iterations are not a power of 2!"); + } + } + + public int getCost() { + double cost = Math.log10(this.getIterations()) / Math.log10(2); + + return (int) cost; + } + + + public static List getAlgorithmsBcrypt() { + return unmodifiableList(ALGORITHMS_BCRYPT); } public static BCryptHash generate(final char[] source) { - return generate(source, createSalt(), DEFAULT_ITERATIONS); + return generate(source, createSalt(), DEFAULT_COST); } @@ -80,6 +124,8 @@ private static BCryptHash fromCryptString(String cryptString) { final String algorithmName = parts[0]; final int cost = Integer.parseInt(parts[1], 10); + final int iterations = (int) Math.pow(2, cost); + final String dataSection = parts[2]; final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); final String saltBase64 = dataSection.substring(0, 22); @@ -87,7 +133,7 @@ private static BCryptHash fromCryptString(String cryptString) { 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); + return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), iterations); } protected static byte[] createSalt() { @@ -104,54 +150,17 @@ public int getSaltLength() { return SALT_LENGTH; } - @Override - public ByteSource getSalt() { - return this.salt; - } - - /** - * Warning! The returned value is actually the cost, not the iterations. - * - * @return the cost. - */ - @Override - public int getIterations() { - return this.getCost(); - } - @Override public boolean matchesPassword(ByteSource plaintextBytes) { - final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, plaintextBytes.getBytes(), this.salt.getBytes(), cost); + final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); return this.equals(fromCryptString(cryptString)); } - public int getRealIterations() { - return (int) Math.pow(2, this.getCost()); - } - - public int getCost() { - return this.cost; - } - - @Override - public byte[] getBytes() { - return Arrays.copyOf(this.hashedData, this.hashedData.length); - } - - @Override - public boolean isEmpty() { - return false; - } - @Override public String toString() { return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]") .add("super=" + super.toString()) - .add("version='" + this.version + "'") - .add("salt=" + Arrays.toString(this.salt.getBytes())) - .add("hashedData=" + Arrays.toString(this.hashedData)) - .add("cost=" + this.cost) .toString(); } } 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 52a6ecb9e2..0ebfef5a38 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 @@ -28,8 +28,6 @@ import org.apache.shiro.lang.util.StringUtils; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; /** * The {@code Shiro1CryptFormat} is a fully reversible @@ -96,7 +94,6 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat public static final String ID = "shiro1"; public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER; - private static final List ALGORITHMS_BCRYPT = Arrays.asList("2", "2a", "2b", "2y"); public Shiro1CryptFormat() { } @@ -150,7 +147,7 @@ public Hash parse(final String formatted) { final String algorithmName = parts[i]; final byte[] digest; - if (ALGORITHMS_BCRYPT.contains(algorithmName)) { + if (BCryptHash.getAlgorithmsBcrypt().contains(algorithmName)) { digest = new OpenBSDBase64.Default().decode(digestBase64.getBytes(StandardCharsets.ISO_8859_1)); } else { digest = Base64.decode(digestBase64); @@ -170,6 +167,7 @@ public Hash parse(final String formatted) { case "2a": case "2b": case "2y": + // bcrypt return new BCryptHash(algorithmName, digest, salt, iterations); default: final SimpleHash hash = new SimpleHash(algorithmName); diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy index 54d8c9aead..dbba4af543 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy @@ -17,43 +17,46 @@ * under the License. */ -package org.apache.shiro.crypto.hash; +package org.apache.shiro.crypto.hash -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Test -import java.security.SecureRandom; +import java.security.SecureRandom -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static java.lang.Math.pow +import static org.junit.jupiter.api.Assertions.assertArrayEquals +import static org.junit.jupiter.api.Assertions.assertEquals -public class BCryptHashTest { +class BCryptHashTest { private static final String TEST_PASSWORD = "secret#shiro,password;Jo8opech"; @Test - public void testCreateHashGenerateSaltIterations() { + void testCreateHashGenerateSaltIterations() { // given - final char[] testPasswordChars = TEST_PASSWORD.toCharArray(); + final def testPasswordChars = TEST_PASSWORD.toCharArray(); // when - final BCryptHash bCryptHash = BCryptHash.generate(testPasswordChars); + final def bCryptHash = BCryptHash.generate testPasswordChars; // then - assertEquals(BCryptHash.DEFAULT_ITERATIONS, bCryptHash.getIterations()); + assertEquals BCryptHash.DEFAULT_COST, bCryptHash.cost; } @Test - public void testCreateHashGivenSalt() { + void testCreateHashGivenSalt() { // given - final char[] testPasswordChars = TEST_PASSWORD.toCharArray(); - final byte[] salt = new SecureRandom().generateSeed(16); + final def testPasswordChars = TEST_PASSWORD.toCharArray(); + final def salt = new SecureRandom().generateSeed 16; + final def cost = 6 // when - final BCryptHash bCryptHash = BCryptHash.generate(testPasswordChars, salt, 6); + final def bCryptHash = BCryptHash.generate(testPasswordChars, salt, cost); // then - assertEquals(6, bCryptHash.getIterations()); - assertArrayEquals(salt, bCryptHash.getSalt().getBytes()); + assertEquals cost, bCryptHash.cost; + assertEquals pow(2, cost) as int, bCryptHash.iterations; + assertArrayEquals salt, bCryptHash.salt.bytes; } } From e19b756c7da98555251f01ef0b39c4d3e4c2eeda Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Mon, 4 Jan 2021 10:00:10 +0100 Subject: [PATCH 05/11] [SHIRO-290] Implement Argon2Hash.java. - expand iterations field to take a comma separated list. Maybe just create a Shiro2CryptFormat instead? - Hex and Base64 formats are not fixed. Maybe we can drop them? - Fixed parameter "algorithm name" not taken into account for bcrypt. - Allow Hasher to read from stdin - Added a short test for Hasher.java. - Changed default DefaultPasswordService.java algorithm to "Argon2id". --- .../credential/DefaultPasswordService.java | 5 +- .../shiro/crypto/hash/AbstractCryptHash.java | 4 +- .../apache/shiro/crypto/hash/Argon2Hash.java | 126 ++++++++++++++---- .../apache/shiro/crypto/hash/BCryptHash.java | 40 +++--- .../shiro/crypto/hash/DefaultHashService.java | 30 +++++ .../crypto/hash/format/Shiro1CryptFormat.java | 71 ++++++++-- .../shiro/crypto/hash/Argon2HashTest.groovy | 44 ++++++ .../org/apache/shiro/tools/hasher/Hasher.java | 26 ++-- .../apache/shiro/tools/hasher/HasherTest.java | 56 ++++++++ 9 files changed, 339 insertions(+), 63 deletions(-) create mode 100644 crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy create mode 100644 tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java 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 08f11c3388..cde0513543 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,6 +18,7 @@ */ package org.apache.shiro.authc.credential; +import org.apache.shiro.crypto.hash.Argon2Hash; import org.apache.shiro.crypto.hash.DefaultHashService; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.HashRequest; @@ -45,8 +46,8 @@ */ 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"; + public static final int DEFAULT_HASH_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS; private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); 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 index c52c25babb..2bfcb5a94e 100644 --- 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 @@ -93,7 +93,9 @@ private void checkValidSalt() { * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash. */ @Override - public abstract String getAlgorithmName(); + public String getAlgorithmName() { + return this.version; + } /** * The length in number of bytes of the salt which is needed for this algorithm. diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java index 4d87774da3..98405f2080 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java @@ -28,19 +28,32 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.StringJoiner; import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; +/** + * The Argon2 key derivation function (KDF) is a modern algorithm to shade and hash passwords. + * + *

The default implementation ({@code argon2id}) is designed to use both memory and cpu to make + * brute force attacks unfeasible.

+ * + *

The defaults are taken from + * argon2-cffi.readthedocs.io. + * The RFC suggests to use 1 GiB of memory for frontend and 4 GiB for backend authentication.

+ */ public class Argon2Hash extends AbstractCryptHash { private static final long serialVersionUID = 2647354947284558921L; private static final String ALGORITHM_NAME = "argon2id"; - private static final List ALGORITHMS_ARGON2 = Arrays.asList("argon2id", "argon2i", "argon2d"); + public static final int DEFAULT_ITERATIONS = 4; - private static final int DEFAULT_ITERATIONS = 10; + private static final List ALGORITHMS_ARGON2 = Arrays.asList("argon2id", "argon2i", "argon2d"); - private static final int DEFAULT_MEMORY = 1_048_576; + private static final int DEFAULT_MEMORY = 1_048_576 / 4; private static final int DEFAULT_PARALLELISM = 4; @@ -49,43 +62,75 @@ public class Argon2Hash extends AbstractCryptHash { */ private static final int SALT_LENGTH = 16; - public Argon2Hash(byte[] hashedData, ByteSource salt, int cost) { - super(ALGORITHM_NAME, hashedData, salt, cost); + private final int memoryKiB; + + private final int parallelism; + + public Argon2Hash(byte[] hashedData, ByteSource salt, int iterations, int memoryKiB, int parallelism) { + super(ALGORITHM_NAME, hashedData, salt, iterations); + this.memoryKiB = memoryKiB; + this.parallelism = parallelism; } - public Argon2Hash(String version, byte[] hashedData, ByteSource salt, int cost) { - super(version, hashedData, salt, cost); + public Argon2Hash(String version, byte[] hashedData, ByteSource salt, int iterations, int memoryKiB, int parallelism) { + super(version, hashedData, salt, iterations); + this.memoryKiB = memoryKiB; + this.parallelism = parallelism; } public static List getAlgorithmsArgon2() { return unmodifiableList(ALGORITHMS_ARGON2); } + public static ByteSource createSalt() { + return new SimpleByteSource(new SecureRandom().generateSeed(SALT_LENGTH)); + } + public static Argon2Hash generate(final char[] source) { - return generate(source, createSalt(), DEFAULT_ITERATIONS); + return generate(new SimpleByteSource(source), createSalt(), DEFAULT_ITERATIONS); } - public static byte[] createSalt() { - return new SecureRandom().generateSeed(SALT_LENGTH); + public static Argon2Hash generate(final ByteSource source, final ByteSource salt, final int iterations) { + return generate(ALGORITHM_NAME, source, requireNonNull(salt, "salt"), iterations); } + public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations) { + return generate(algorithmName, source, salt, DEFAULT_ITERATIONS, DEFAULT_MEMORY, DEFAULT_PARALLELISM); + } - public static Argon2Hash generate(final char[] source, final byte[] salt, final int iterations) { - final Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) + public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations, int memoryKiB, int parallelism) { + 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 UnsupportedOperationException("Unknown argon2 algorithm: " + algorithmName); + } + + final Argon2Parameters parameters = new Argon2Parameters.Builder(type) .withVersion(Argon2Parameters.ARGON2_VERSION_13) .withIterations(iterations) - .withMemoryAsKB(DEFAULT_MEMORY) - .withParallelism(DEFAULT_PARALLELISM) - .withSalt(salt) + .withMemoryAsKB(memoryKiB) + .withParallelism(parallelism) + .withSalt(requireNonNull(salt, "salt").getBytes()) .build(); final Argon2BytesGenerator gen = new Argon2BytesGenerator(); gen.init(parameters); final byte[] hash = new byte[32]; - gen.generateBytes(source, hash); + gen.generateBytes(source.getBytes(), hash); - return new Argon2Hash(ALGORITHM_NAME, hash, new SimpleByteSource(salt), iterations); + return new Argon2Hash(algorithmName, hash, new SimpleByteSource(salt), iterations, memoryKiB, parallelism); } @Override @@ -116,21 +161,50 @@ protected void checkValidIterations() { } @Override - public String getAlgorithmName() { - // TODO: implement - throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::getAlgorithmName]."); + public boolean matchesPassword(ByteSource plaintextBytes) { + Argon2Hash compare = generate(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism); + return this.equals(compare); } @Override - public boolean matchesPassword(ByteSource plaintextBytes) { - // TODO: implement - throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::matchesPassword]."); + public int getSaltLength() { + return SALT_LENGTH; + } + + public int getMemoryKiB() { + return memoryKiB; + } + + public int getParallelism() { + return parallelism; } @Override - public int getSaltLength() { - // TODO: implement - throw new UnsupportedOperationException("not yet implemented: [org.apache.shiro.crypto.hash.Argon2Hash::getSaltLength]."); + 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 memoryKiB == that.memoryKiB && parallelism == that.parallelism; } + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), memoryKiB, parallelism); + } + + @Override + public String toString() { + return new StringJoiner(", ", Argon2Hash.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("memoryKiB=" + memoryKiB) + .add("parallelism=" + parallelism) + .toString(); + } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java index 7a2a78eeff..646308a77b 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -72,9 +72,13 @@ protected final void checkValidAlgorithm() { @Override protected final void checkValidIterations() { - double costDbl = Math.log10(this.getIterations()) / Math.log10(2); + checkValidIterations(this.getIterations()); + } + + protected static void checkValidIterations(int iterations) { + double costDbl = Math.log10(iterations) / Math.log10(2); if ((costDbl != Math.floor(costDbl)) || Double.isInfinite(costDbl)) { - throw new IllegalArgumentException("Iterations are not a power of 2. Found: [" + this.getIterations() + "]."); + throw new IllegalArgumentException("Iterations are not a power of 2. Found: [" + iterations + "]."); } int cost = (int) costDbl; @@ -87,14 +91,19 @@ protected final void checkValidIterations() { throw new IllegalArgumentException(message); } - double iterations = Math.pow(2, cost); - if (iterations != getIterations()) { + double iterationCount = Math.pow(2, cost); + if (iterations != iterationCount) { throw new IllegalArgumentException("Iterations are not a power of 2!"); } } public int getCost() { - double cost = Math.log10(this.getIterations()) / Math.log10(2); + return getCostFromIterations(this.getIterations()); + } + + public static int getCostFromIterations(final int iterations) { + checkValidIterations(iterations); + double cost = Math.log10(iterations) / Math.log10(2); return (int) cost; } @@ -104,13 +113,17 @@ public static List getAlgorithmsBcrypt() { return unmodifiableList(ALGORITHMS_BCRYPT); } - public static BCryptHash generate(final char[] source) { + public static BCryptHash generate(final ByteSource source) { return generate(source, createSalt(), DEFAULT_COST); } - public static BCryptHash generate(final char[] source, final byte[] initialSalt, final int cost) { - final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, source, initialSalt, cost); + public static BCryptHash generate(final ByteSource source, final ByteSource initialSalt, final int cost) { + return generate(ALGORITHM_NAME, source, initialSalt, cost); + } + + public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) { + final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost); return fromCryptString(cryptString); } @@ -136,13 +149,8 @@ private static BCryptHash fromCryptString(String cryptString) { return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), iterations); } - protected static byte[] createSalt() { - return new SecureRandom().generateSeed(SALT_LENGTH); - } - - @Override - public String getAlgorithmName() { - return ALGORITHM_NAME; + protected static ByteSource createSalt() { + return new SimpleByteSource(new SecureRandom().generateSeed(SALT_LENGTH)); } @Override @@ -152,7 +160,7 @@ public int getSaltLength() { @Override public boolean matchesPassword(ByteSource plaintextBytes) { - final String cryptString = OpenBSDBCrypt.generate(ALGORITHM_NAME, plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); + final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); return this.equals(fromCryptString(cryptString)); } 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 c082ca2eb2..4b763a168d 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 @@ -23,6 +23,8 @@ import org.apache.shiro.lang.util.ByteSource; import org.apache.shiro.lang.util.SimpleByteSource; +import java.util.Optional; + /** * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name, * secure-random salt generation, multiple hash iterations and an optional internal @@ -162,6 +164,34 @@ public Hash computeHash(HashRequest request) { ByteSource privateSalt = getPrivateSalt(); ByteSource salt = combine(privateSalt, publicSalt); + switch (algorithmName) { + case "2": + case "2a": + case "2b": + case "2y": + // bcrypt + ByteSource bcryptSalt = Optional.ofNullable(publicSalt).orElseGet(() -> BCryptHash.createSalt()); + return createBcryptHash(algorithmName, source, bcryptSalt, iterations); + case "argon2": + case "argon2d": + case "argon2i": + case "argon2id": + ByteSource argon2Salt = Optional.ofNullable(publicSalt).orElseGet(() -> Argon2Hash.createSalt()); + return createArgon2Hash(algorithmName, source, iterations, argon2Salt); + default: + return createSimpleHash(algorithmName, source, iterations, publicSalt, salt); + } + } + + private BCryptHash createBcryptHash(String algorithmName, ByteSource source, ByteSource salt, int iterations) { + return BCryptHash.generate(algorithmName, source, salt, iterations); + } + + private Argon2Hash createArgon2Hash(String algorithmName, ByteSource source, int iterations, ByteSource salt) { + return Argon2Hash.generate(algorithmName, source, salt, iterations); + } + + 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); 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 0ebfef5a38..2da2f0a3ff 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,6 +18,7 @@ */ package org.apache.shiro.crypto.hash.format; +import org.apache.shiro.crypto.hash.Argon2Hash; import org.apache.shiro.crypto.hash.BCryptHash; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.SimpleHash; @@ -28,6 +29,7 @@ import org.apache.shiro.lang.util.StringUtils; import java.nio.charset.StandardCharsets; +import java.util.Locale; /** * The {@code Shiro1CryptFormat} is a fully reversible @@ -68,7 +70,8 @@ * * 3 * {@code iterationCount} - * The number of hash iterations performed. + * The number of hash iterations performed. In case of argon2, this is a comma separated list, + * containing the number of iterations, the memory in kiB and the parallelism count. * true (1 <= N <= Integer.MAX_VALUE) * * @@ -111,8 +114,9 @@ public String format(final Hash hash) { final String algorithmName = hash.getAlgorithmName(); final ByteSource salt = hash.getSalt(); - final int iterations = hash.getIterations(); - final StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); + String iterationParameter = formatIterationParameter(hash); + + final StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterationParameter).append(TOKEN_DELIMITER); if (salt != null) { sb.append(salt.toBase64()); @@ -124,6 +128,22 @@ public String format(final Hash hash) { return sb.toString(); } + private String formatIterationParameter(Hash hash) { + if (hash instanceof Argon2Hash) { + Argon2Hash argon2Hash = (Argon2Hash) hash; + + return String.format( + Locale.ENGLISH, + "%d,%d,%d", + hash.getIterations(), + argon2Hash.getMemoryKiB(), + argon2Hash.getParallelism() + ); + } + + return "" + hash.getIterations(); + } + @Override public Hash parse(final String formatted) { if (formatted == null) { @@ -154,13 +174,8 @@ public Hash parse(final String formatted) { } ByteSource salt = parseSalt(saltBase64, algorithmName); - final int iterations; - try { - iterations = Integer.parseInt(iterationsString); - } catch (final NumberFormatException e) { - final String msg = "Unable to parse formatted hash string: " + formatted; - throw new IllegalArgumentException(msg, e); - } + String[] iterationsParameter = iterationsString.split(","); + final int iterations = parseIterations(formatted, iterationsParameter); switch (algorithmName) { case "2": @@ -169,6 +184,15 @@ public Hash parse(final String formatted) { case "2y": // bcrypt return new BCryptHash(algorithmName, digest, salt, iterations); + case "argon2": + case "argon2d": + case "argon2i": + case "argon2id": + // argon2 + // also needsmemory and parallelism + int memoryKiB = parseMemory(formatted, iterationsParameter); + int paralellism = parseParallelism(formatted, iterationsParameter); + return new Argon2Hash(algorithmName, digest, salt, iterations, memoryKiB, paralellism); default: final SimpleHash hash = new SimpleHash(algorithmName); hash.setBytes(digest); @@ -179,6 +203,33 @@ public Hash parse(final String formatted) { } } + private int parseParallelism(String formatted, String[] iterationsParameter) { + try { + return Integer.parseInt(iterationsParameter[2]); + } catch (final NumberFormatException | IndexOutOfBoundsException e) { + final String msg = "Unable to parse formatted hash string: " + formatted; + throw new IllegalArgumentException(msg, e); + } + } + + private int parseMemory(String formatted, String[] iterationsParameter) { + try { + return Integer.parseInt(iterationsParameter[1]); + } catch (final NumberFormatException | IndexOutOfBoundsException e) { + final String msg = "Unable to parse formatted hash string: " + formatted; + throw new IllegalArgumentException(msg, e); + } + } + + private int parseIterations(String formatted, String[] iterationsParameter) { + try { + return Integer.parseInt(iterationsParameter[0]); + } catch (final NumberFormatException e) { + final String msg = "Unable to parse formatted hash string: " + formatted; + throw new IllegalArgumentException(msg, e); + } + } + private ByteSource parseSalt(String base64, String algorithmName) { if (!StringUtils.hasLength(base64)) { return SimpleByteSource.empty(); diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy new file mode 100644 index 0000000000..08684d1bae --- /dev/null +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy @@ -0,0 +1,44 @@ +/* + * 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.SimpleByteSource +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertTrue + +class Argon2HashTest { + + @Test + void testArgon2Hash() { + // given + def shiro1Format = '$shiro1$argon2id$2,131072,4$7858qTJTreh61AzFV2XMOw==$lLzl2VNNbyFcuJo0Hp7JQpguKCDoQwxo91AWobcHzeo=' + def expectedPassword = new SimpleByteSource('secret#shiro,password;Jo8opech') + + // when + def hash = new Shiro1CryptFormat().parse shiro1Format; + def matchesPassword = hash.matchesPassword expectedPassword; + + // then + assertTrue matchesPassword + } + +} 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 e20315303a..91ee0bad33 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; @@ -38,12 +36,16 @@ 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.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 java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.util.Arrays; /** @@ -66,7 +68,7 @@ public final class Hasher { private static final int DEFAULT_NUM_ITERATIONS = 1; private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_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 HELP = new Option("help", "help", false, "show this help message."); @@ -441,12 +443,20 @@ private static void printHelpAndExit(Options options, Exception e, boolean debug 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/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 0000000000..5416de8597 --- /dev/null +++ b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java @@ -0,0 +1,56 @@ +/* + * 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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +public class HasherTest { + + private final InputStream systemIn = System.in; + + private ByteArrayInputStream testIn; + + @BeforeEach + public void setUpOutput() { + } + + private void provideInput(String data) { + testIn = new ByteArrayInputStream(data.getBytes()); + System.setIn(testIn); + } + + @AfterEach + public void restoreSystemInputOutput() { + System.setIn(systemIn); + } + + + @Test + public void testArgon2Hash() { + String[] args = {"--debug", "--password", "--pnoconfirm"}; + provideInput("secret#shiro,password;Jo8opech"); + Hasher.main(args); + } +} From 52dc6308a0c1d8af2549e8f42bc8c1af07a98214 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Thu, 7 Jan 2021 09:04:26 +0100 Subject: [PATCH 06/11] [SHIRO-290] Implement Shiro2CryptFormat.java. - Only fields 1 and two are defined, rest is defined by the hash implementation - Therefore fully backwards-compatible to Shiro1CryptFormat.java. - Loads formats from ProvidedKdfHashes.java. We could also think of a pluggable mechanism, like using service loaders to hide classes like OpenBSDBase64. - In AbstractCryptHash.java, renamed `version` to `algorithmName`. - Removed iterations from AbstractCryptHash.java, they are possibly an implementation detail not present in other implementations (like bcrypt). - Signature change: `PasswordService.encryptPassword(Object plaintext)` will now throw a NullPointerException on `null` parameter. It was never specified how this method would behave. --- .../credential/DefaultPasswordService.java | 8 +- .../DefaultPasswordServiceTest.groovy | 8 +- .../credential/PasswordMatcherTest.groovy | 24 ++- .../shiro/crypto/hash/AbstractCryptHash.java | 47 +++-- .../apache/shiro/crypto/hash/Argon2Hash.java | 161 +++++++++++++++--- .../apache/shiro/crypto/hash/BCryptHash.java | 113 ++++++------ .../hash/format/ProvidedHashFormat.java | 7 +- .../crypto/hash/format/ProvidedKdfHashes.java | 82 +++++++++ .../crypto/hash/format/Shiro1CryptFormat.java | 61 +++---- .../crypto/hash/format/Shiro2CryptFormat.java | 135 +++++++++++++++ .../shiro/crypto/hash/Argon2HashTest.groovy | 50 +++++- .../shiro/crypto/hash/BCryptHashTest.groovy | 44 ++++- .../hash/format/ProvidedHashFormatTest.groovy | 2 +- 13 files changed, 578 insertions(+), 164 deletions(-) create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java 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 cde0513543..45c8247c00 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 @@ -27,13 +27,15 @@ 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.Shiro1CryptFormat; +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: @@ -66,13 +68,13 @@ public DefaultPasswordService() { hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure 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); } 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 5365e75b7d..1c97aa26aa 100644 --- a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy +++ b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy @@ -23,10 +23,11 @@ import org.apache.shiro.crypto.hash.* import org.apache.shiro.crypto.hash.format.HashFormatFactory import org.apache.shiro.crypto.hash.format.HexFormat import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat -import org.junit.Test +import org.junit.jupiter.api.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. @@ -38,7 +39,8 @@ class DefaultPasswordServiceTest { @Test void testEncryptPasswordWithNullArgument() { def service = new DefaultPasswordService() - assertNull service.encryptPassword(null) + + assertThrows(NullPointerException, { service.encryptPassword(null) } as Executable) } @Test 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 7edf1c0d47..c0d3e7cc41 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 @@ -23,7 +23,7 @@ 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.Shiro1CryptFormat +import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat import org.junit.Test import static java.lang.Math.pow @@ -177,11 +177,29 @@ class PasswordMatcherTest { @Test 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 + void testArgon2Password() { // given def matcher = new PasswordMatcher(); def iterations = (int) pow(2, 10) - def bcryptPw = '$shiro1$2y$' + iterations + '$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' - def bcryptHash = new Shiro1CryptFormat().parse(bcryptPw); + 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) 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 index 2bfcb5a94e..29eb70f207 100644 --- 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 @@ -20,7 +20,6 @@ package org.apache.shiro.crypto.hash; import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.CodecSupport; import org.apache.shiro.lang.codec.Hex; import org.apache.shiro.lang.util.ByteSource; @@ -29,16 +28,20 @@ import java.util.Arrays; import java.util.Locale; import java.util.StringJoiner; +import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; -public abstract class AbstractCryptHash extends CodecSupport implements Hash, Serializable { +public abstract class AbstractCryptHash implements Hash, Serializable { private static final long serialVersionUID = 2483214646921027859L; - private final String version; + + protected static final Pattern DELIMITER = Pattern.compile("\\$"); + + private final String algorithmName; private final byte[] hashedData; private final ByteSource salt; - private final int iterations; + /** * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead. */ @@ -48,11 +51,10 @@ public abstract class AbstractCryptHash extends CodecSupport implements Hash, Se */ private String base64Encoded; - public AbstractCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) { - this.version = version; + 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); - this.iterations = cost; checkValid(); } @@ -60,8 +62,6 @@ protected final void checkValid() { checkValidAlgorithm(); checkValidSalt(); - - checkValidIterations(); } protected abstract void checkValidAlgorithm(); @@ -79,10 +79,8 @@ private void checkValidSalt() { } } - protected abstract void checkValidIterations(); - /** - * Implemented by subclasses, this specifies the {@link MessageDigest MessageDigest} algorithm name + * Implemented by subclasses, this specifies the KDF algorithm name * to use when performing the hash. * *

When multiple algorithm names are acceptable, then this method should return the primary algorithm name.

@@ -90,11 +88,11 @@ private void checkValidSalt() { *

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.

* - * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash. + * @return the KDF algorithm name to use when performing the hash. */ @Override public String getAlgorithmName() { - return this.version; + return this.algorithmName; } /** @@ -109,16 +107,6 @@ public ByteSource getSalt() { return this.salt; } - /** - * Warning! The returned value is actually the cost, not the iterations. - * - * @return the cost. - */ - @Override - public int getIterations() { - return this.iterations; - } - @Override public byte[] getBytes() { return Arrays.copyOf(this.hashedData, this.hashedData.length); @@ -160,6 +148,14 @@ public String toBase64() { return this.base64Encoded; } + /** + * This method MUST 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 a Hash and its {@link #getBytes byte array} is identical to * this Hash's byte array, {@code false} otherwise. @@ -199,10 +195,9 @@ public int hashCode() { public String toString() { return new StringJoiner(", ", AbstractCryptHash.class.getSimpleName() + "[", "]") .add("super=" + super.toString()) - .add("version='" + version + "'") + .add("algorithmName='" + algorithmName + "'") .add("hashedData=" + Arrays.toString(hashedData)) .add("salt=" + salt) - .add("iterations=" + iterations) .add("hexEncoded='" + hexEncoded + "'") .add("base64Encoded='" + base64Encoded + "'") .toString(); diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java index 98405f2080..c6f10aad3f 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java @@ -19,6 +19,7 @@ package org.apache.shiro.crypto.hash; +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; @@ -26,10 +27,12 @@ import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64.Encoder; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; +import java.util.regex.Pattern; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; @@ -43,39 +46,50 @@ *

The defaults are taken from * argon2-cffi.readthedocs.io. * The RFC suggests to use 1 GiB of memory for frontend and 4 GiB for backend authentication.

+ * + *

Example crypt string is: {@code $argon2i$v=19$m=16384,t=100,p=2$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY}.

*/ public class Argon2Hash extends AbstractCryptHash { private static final long serialVersionUID = 2647354947284558921L; + private static final Pattern DELIMITER_COMMA = Pattern.compile(","); + private static final String ALGORITHM_NAME = "argon2id"; - public static final int DEFAULT_ITERATIONS = 4; + private static final int DEFAULT_VERSION = Argon2Parameters.ARGON2_VERSION_13; + + public static final int DEFAULT_ITERATIONS = 3; private static final List ALGORITHMS_ARGON2 = Arrays.asList("argon2id", "argon2i", "argon2d"); - private static final int DEFAULT_MEMORY = 1_048_576 / 4; + private static final int DEFAULT_MEMORY_POW_2 = 4096; private static final int DEFAULT_PARALLELISM = 4; + private static final int DEFAULT_OUTPUT_LENGTH = 32; + + /** * 128 bits of salt is the recommended salt length. */ private static final int SALT_LENGTH = 16; + private final int argonVersion; + + private final int iterations; + private final int memoryKiB; private final int parallelism; - public Argon2Hash(byte[] hashedData, ByteSource salt, int iterations, int memoryKiB, int parallelism) { - super(ALGORITHM_NAME, hashedData, salt, iterations); - this.memoryKiB = memoryKiB; + 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; - } - public Argon2Hash(String version, byte[] hashedData, ByteSource salt, int iterations, int memoryKiB, int parallelism) { - super(version, hashedData, salt, iterations); - this.memoryKiB = memoryKiB; - this.parallelism = parallelism; + checkValidIterations(); } public static List getAlgorithmsArgon2() { @@ -86,6 +100,65 @@ public static ByteSource createSalt() { return new SimpleByteSource(new SecureRandom().generateSeed(SALT_LENGTH)); } + public static Argon2Hash fromString(String input) { + // expected: + // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY + if (!input.startsWith("$")) { + throw new UnsupportedOperationException("Unsupported input: " + input); + } + + final String[] parts = 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); } @@ -95,11 +168,20 @@ public static Argon2Hash generate(final ByteSource source, final ByteSource salt } public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations) { - return generate(algorithmName, source, salt, DEFAULT_ITERATIONS, DEFAULT_MEMORY, DEFAULT_PARALLELISM); + return generate(algorithmName, DEFAULT_VERSION, source, salt, iterations, DEFAULT_MEMORY_POW_2, DEFAULT_PARALLELISM, DEFAULT_OUTPUT_LENGTH); } - public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations, int memoryKiB, int parallelism) { - int type; + public static Argon2Hash generate( + String algorithmName, + int argonVersion, + ByteSource source, + ByteSource salt, + int iterations, + int memoryAsKB, + int parallelism, + int outputLength + ) { + final int type; switch (requireNonNull(algorithmName, "algorithmName")) { case "argon2i": type = Argon2Parameters.ARGON2_i; @@ -117,20 +199,20 @@ public static Argon2Hash generate(String algorithmName, ByteSource source, ByteS } final Argon2Parameters parameters = new Argon2Parameters.Builder(type) - .withVersion(Argon2Parameters.ARGON2_VERSION_13) + .withVersion(argonVersion) .withIterations(iterations) - .withMemoryAsKB(memoryKiB) .withParallelism(parallelism) .withSalt(requireNonNull(salt, "salt").getBytes()) + .withMemoryAsKB(memoryAsKB) .build(); final Argon2BytesGenerator gen = new Argon2BytesGenerator(); gen.init(parameters); - final byte[] hash = new byte[32]; + final byte[] hash = new byte[outputLength]; gen.generateBytes(source.getBytes(), hash); - return new Argon2Hash(algorithmName, hash, new SimpleByteSource(salt), iterations, memoryKiB, parallelism); + return new Argon2Hash(algorithmName, argonVersion, hash, new SimpleByteSource(salt), iterations, memoryAsKB, parallelism); } @Override @@ -147,7 +229,6 @@ protected void checkValidAlgorithm() { } } - @Override protected void checkValidIterations() { int iterations = this.getIterations(); if (iterations < 1) { @@ -160,9 +241,14 @@ protected void checkValidIterations() { } } + @Override + public int getIterations() { + return this.iterations; + } + @Override public boolean matchesPassword(ByteSource plaintextBytes) { - Argon2Hash compare = generate(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism); + Argon2Hash compare = generate(this.getAlgorithmName(), this.argonVersion, plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism, this.getBytes().length); return this.equals(compare); } @@ -171,6 +257,33 @@ public int getSaltLength() { return SALT_LENGTH; } + @Override + public String formatToCryptString() { + // expected: + // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY + Encoder encoder = java.util.Base64.getEncoder().withoutPadding(); + String saltBase64 = encoder.encodeToString(this.getSalt().getBytes()); + String dataBase64 = encoder.encodeToString(this.getBytes()); + + return new StringJoiner("$", "$", "") + .add(this.getAlgorithmName()) + .add("v=" + this.argonVersion) + .add(formatParameters()) + .add(saltBase64) + .add(dataBase64) + .toString(); + } + + private CharSequence formatParameters() { + return String.format( + Locale.ENGLISH, + "t=%d,m=%d,p=%d", + getIterations(), + getMemoryKiB(), + getParallelism() + ); + } + public int getMemoryKiB() { return memoryKiB; } @@ -179,6 +292,10 @@ public int getParallelism() { return parallelism; } + public int getArgonVersion() { + return argonVersion; + } + @Override public boolean equals(Object other) { if (this == other) { @@ -191,18 +308,20 @@ public boolean equals(Object other) { return false; } Argon2Hash that = (Argon2Hash) other; - return memoryKiB == that.memoryKiB && parallelism == that.parallelism; + return argonVersion == that.argonVersion && iterations == that.iterations && memoryKiB == that.memoryKiB && parallelism == that.parallelism; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), memoryKiB, parallelism); + 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/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java index 646308a77b..497840447a 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Locale; import java.util.StringJoiner; -import java.util.regex.Pattern; import static java.util.Collections.unmodifiableList; @@ -44,16 +43,17 @@ public class BCryptHash extends AbstractCryptHash { private static final int SALT_LENGTH = 16; - private static final Pattern DELIMITER = Pattern.compile("\\$"); - private static final List ALGORITHMS_BCRYPT = Arrays.asList("2", "2a", "2b", "2y"); - public BCryptHash(final byte[] hashedData, final ByteSource salt, final int iterations) { - this(ALGORITHM_NAME, hashedData, salt, iterations); - } + private final int cost; - public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int iterations) { - super(version, hashedData, salt, iterations); + 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 @@ -70,18 +70,7 @@ protected final void checkValidAlgorithm() { } } - @Override - protected final void checkValidIterations() { - checkValidIterations(this.getIterations()); - } - - protected static void checkValidIterations(int iterations) { - double costDbl = Math.log10(iterations) / Math.log10(2); - if ((costDbl != Math.floor(costDbl)) || Double.isInfinite(costDbl)) { - throw new IllegalArgumentException("Iterations are not a power of 2. Found: [" + iterations + "]."); - } - - int cost = (int) costDbl; + protected final void checkValidCost() { if (cost < 4 || cost > 31) { final String message = String.format( Locale.ENGLISH, @@ -90,63 +79,55 @@ protected static void checkValidIterations(int iterations) { ); throw new IllegalArgumentException(message); } - - double iterationCount = Math.pow(2, cost); - if (iterations != iterationCount) { - throw new IllegalArgumentException("Iterations are not a power of 2!"); - } } public int getCost() { - return getCostFromIterations(this.getIterations()); - } - - public static int getCostFromIterations(final int iterations) { - checkValidIterations(iterations); - double cost = Math.log10(iterations) / Math.log10(2); - - return (int) cost; + return this.cost; } - public static List getAlgorithmsBcrypt() { return unmodifiableList(ALGORITHMS_BCRYPT); } - 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(ALGORITHM_NAME, source, initialSalt, cost); - } - - public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) { - final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost); - - return fromCryptString(cryptString); - } + public static BCryptHash fromString(String input) { + // the input string should look like this: + // $2y$cost$salt{22}hash + if (!input.startsWith("$")) { + throw new UnsupportedOperationException("Unsupported input: " + input); + } - private static BCryptHash fromCryptString(String cryptString) { - String[] parts = DELIMITER.split(cryptString.substring(1), -1); + final String[] parts = 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]; - final int cost = Integer.parseInt(parts[1], 10); - final int iterations = (int) Math.pow(2, cost); + 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), iterations); + 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(ALGORITHM_NAME, source, initialSalt, cost); + } + + public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) { + final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost); + + return fromString(cryptString); } protected static ByteSource createSalt() { @@ -158,17 +139,37 @@ 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) { final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); + BCryptHash other = fromString(cryptString); - return this.equals(fromCryptString(cryptString)); + return this.equals(other); } @Override public String toString() { return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]") .add("super=" + super.toString()) + .add("cost=" + this.cost) .toString(); } } 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 6e22a728a4..9ed5246d86 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,7 +40,12 @@ public enum ProvidedHashFormat { /** * Value representing the {@link Shiro1CryptFormat} implementation. */ - SHIRO1(Shiro1CryptFormat.class); + SHIRO1(Shiro1CryptFormat.class), + + /** + * Value representing the {@link Shiro2CryptFormat} implementation. + */ + SHIRO2(Shiro2CryptFormat.class); private final Class clazz; diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java new file mode 100644 index 0000000000..09736abf9a --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java @@ -0,0 +1,82 @@ +/* + * 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.Argon2Hash; +import org.apache.shiro.crypto.hash.BCryptHash; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.function.Function; + +import static java.util.Collections.unmodifiableList; + +/** + * Hashes used by the Shiro2CryptFormat class. + */ +public enum ProvidedKdfHashes { + ARGON2( + new String[]{"argon2id", "argon2i", "argon2d"}, + Argon2Hash::fromString + ), + BCRYPT( + new String[]{"2", "2y", "2a", "2b"}, + BCryptHash::fromString + ); + + private final List recognizedAlgorithms; + + private final Function fromStringMethod; + + ProvidedKdfHashes(String[] recognizedAlgorithms, Function fromStringMethod) { + this.recognizedAlgorithms = unmodifiableList(Arrays.asList(recognizedAlgorithms)); + this.fromStringMethod = fromStringMethod; + } + + public static Optional getByAlgorithmName(String algorithmName) { + return Arrays.stream(values()) + .filter(val -> val.getRecognizedAlgorithms().contains(algorithmName)) + .findAny(); + } + + public List getRecognizedAlgorithms() { + return recognizedAlgorithms; + } + + public Function getFromStringMethod() { + return fromStringMethod; + } + + public AbstractCryptHash fromString(String input) { + return getFromStringMethod().apply(input); + } + + @Override + public String toString() { + return new StringJoiner(", ", ProvidedKdfHashes.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("recognizedAlgorithms=" + recognizedAlgorithms) + .add("fromStringMethod=" + fromStringMethod) + .toString(); + } +} 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 2da2f0a3ff..0e6bae451d 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 @@ -164,18 +164,7 @@ public Hash parse(final String formatted) { //second-to-last part is always the salt, Base64-encoded: final String saltBase64 = parts[i--]; final String iterationsString = parts[i--]; - final String algorithmName = parts[i]; - - final byte[] digest; - if (BCryptHash.getAlgorithmsBcrypt().contains(algorithmName)) { - digest = new OpenBSDBase64.Default().decode(digestBase64.getBytes(StandardCharsets.ISO_8859_1)); - } else { - digest = Base64.decode(digestBase64); - } - ByteSource salt = parseSalt(saltBase64, algorithmName); - - String[] iterationsParameter = iterationsString.split(","); - final int iterations = parseIterations(formatted, iterationsParameter); + final String algorithmName = parts[0]; switch (algorithmName) { case "2": @@ -183,51 +172,39 @@ public Hash parse(final String formatted) { case "2b": case "2y": // bcrypt - return new BCryptHash(algorithmName, digest, salt, iterations); + throw new UnsupportedOperationException("bcrypt is not supported in shiro1 format."); case "argon2": case "argon2d": case "argon2i": case "argon2id": // argon2 - // also needsmemory and parallelism - int memoryKiB = parseMemory(formatted, iterationsParameter); - int paralellism = parseParallelism(formatted, iterationsParameter); - return new Argon2Hash(algorithmName, digest, salt, iterations, memoryKiB, paralellism); + throw new UnsupportedOperationException("argon2 is not supported in shiro1 format."); default: - final SimpleHash hash = new SimpleHash(algorithmName); - hash.setBytes(digest); - hash.setSalt(salt); - hash.setIterations(iterations); - - return hash; + // continue parsing } - } - private int parseParallelism(String formatted, String[] iterationsParameter) { - try { - return Integer.parseInt(iterationsParameter[2]); - } catch (final NumberFormatException | IndexOutOfBoundsException e) { - final String msg = "Unable to parse formatted hash string: " + formatted; - throw new IllegalArgumentException(msg, e); - } - } - - private int parseMemory(String formatted, String[] iterationsParameter) { - try { - return Integer.parseInt(iterationsParameter[1]); - } catch (final NumberFormatException | IndexOutOfBoundsException e) { - final String msg = "Unable to parse formatted hash string: " + formatted; - throw new IllegalArgumentException(msg, e); + final byte[] digest; + if (BCryptHash.getAlgorithmsBcrypt().contains(algorithmName)) { + digest = new OpenBSDBase64.Default().decode(digestBase64.getBytes(StandardCharsets.ISO_8859_1)); + } else { + digest = Base64.decode(digestBase64); } - } + ByteSource salt = parseSalt(saltBase64, algorithmName); - private int parseIterations(String formatted, String[] iterationsParameter) { + final int iterations; try { - return Integer.parseInt(iterationsParameter[0]); + iterations = Integer.parseInt(iterationsString); } catch (final NumberFormatException e) { final String msg = "Unable to parse formatted hash string: " + formatted; throw new IllegalArgumentException(msg, e); } + + final SimpleHash hash = new SimpleHash(algorithmName); + hash.setBytes(digest); + hash.setSalt(salt); + hash.setIterations(iterations); + + return hash; } private ByteSource parseSalt(String base64, String algorithmName) { 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 0000000000..5e16b664cd --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java @@ -0,0 +1,135 @@ +/* + * 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.SimpleHash; + +import static java.util.Objects.requireNonNull; + +/** + * The {@code Shiro1CryptFormat} is a fully reversible + * Modular Crypt Format (MCF). It is based + * on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems. + *

Format

+ *

Hash instances formatted with this implementation will result in a String with the following dollar-sign ($) + * delimited format:

+ *
+ * $mcfFormatId$algorithmName$algorithm-specific-data.
+ * 
+ *

Each token is defined as follows:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PositionTokenDescriptionRequired?
1{@code mcfFormatId}The Modular Crypt Format identifier for this implementation, equal to {@code shiro2}. + * ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix + * {@code $shiro2$} ).true
2{@code algorithmName}The name of the hash algorithm used to perform the hash. Either a hash class exists, or + * otherwise a {@link UnsupportedOperationException} will be thrown. + * true
3{@code algorithm-specific-data}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.
+ * + * @see ModularCryptFormat + * @see ParsableHashFormat + * @since 1.2 + */ +public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat { + + public static final String ID = "shiro2"; + 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}. + * + *

This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.

+ * + * @param hash the hash instance to format into a String. + * @return a string representing the hash. + */ + @Override + public String format(final Hash hash) { + requireNonNull(hash, "hash in Shiro2CryptFormat.format(Hash hash)"); + + // backwards compatibility until Shiro 2.1.0. + if (hash instanceof SimpleHash) { + return new Shiro1CryptFormat().format(hash); + } + + if (!(hash instanceof AbstractCryptHash)) { + throw new UnsupportedOperationException("Shiro2CryptFormat can only format classes extending AbstractCryptHash."); + } + + AbstractCryptHash cryptHash = (AbstractCryptHash) hash; + return TOKEN_DELIMITER + ID + cryptHash.formatToCryptString(); + } + + @Override + public Hash parse(final String formatted) { + requireNonNull(formatted, "formatted in Shiro2CryptFormat.parse(String formatted)"); + + // backwards compatibility until Shiro 2.1.0. + if (formatted.startsWith(Shiro1CryptFormat.MCF_PREFIX)) { + return new Shiro1CryptFormat().parse(formatted); + } + + if (!formatted.startsWith(MCF_PREFIX)) { + final String msg = "The argument is not a valid '" + ID + "' formatted hash."; + throw new IllegalArgumentException(msg); + } + + final String suffix = formatted.substring(MCF_PREFIX.length()); + final String[] parts = suffix.split("\\$"); + final String algorithmName = parts[0]; + + ProvidedKdfHashes kdfHash = ProvidedKdfHashes.getByAlgorithmName(algorithmName) + .orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented.")); + return kdfHash.fromString("$" + suffix); + } + +} diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy index 08684d1bae..8f9e305fe1 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy @@ -20,25 +20,69 @@ package org.apache.shiro.crypto.hash 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.assertTrue +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 shiro1Format = '$shiro1$argon2id$2,131072,4$7858qTJTreh61AzFV2XMOw==$lLzl2VNNbyFcuJo0Hp7JQpguKCDoQwxo91AWobcHzeo=' + def shiro2Format = '$shiro2$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI' def expectedPassword = new SimpleByteSource('secret#shiro,password;Jo8opech') // when - def hash = new Shiro1CryptFormat().parse shiro1Format; + def hash = new Shiro2CryptFormat().parse(shiro2Format) as Argon2Hash; + System.out.println("Hash: " + hash) def matchesPassword = hash.matchesPassword expectedPassword; // then + assertEquals Argon2Parameters.ARGON2_VERSION_13, hash.argonVersion + assertEquals 3, hash.iterations + assertEquals 4096, hash.memoryKiB + assertEquals 4, hash.parallelism assertTrue matchesPassword } + @Test + void testArgon2HashShiro1Format() { + // given + def shiro1Format = '$shiro1$argon2id$v=19$t=2,m=131072,p=4$7858qTJTreh61AzFV2XMOw==$lLzl2VNNbyFcuJo0Hp7JQpguKCDoQwxo91AWobcHzeo=' + + // when + def thrownException = assertThrows( + UnsupportedOperationException, + { new Shiro1CryptFormat().parse shiro1Format } as Executable + ) + + // then + assertTrue thrownException.getMessage().contains("shiro1") + } + + @Test + void testFromStringMatchesPw() { + // when + def argon2String = '$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI' + // for testing recreated salt and data parts, as the parameter order could change. + def saltDataPart = argon2String.substring(30) + + // when + def argon2Hash = Argon2Hash.fromString argon2String + def recreatedSaltDataPart = argon2Hash.formatToCryptString().substring(30) + + // then + assertTrue argon2Hash.matchesPassword(TEST_PASSWORD_BS) + // we can only test the salt + data parts, as + // the parameter order could change. + assertEquals saltDataPart, recreatedSaltDataPart + } + } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy index dbba4af543..93520582a1 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy @@ -19,13 +19,16 @@ package org.apache.shiro.crypto.hash +import org.apache.shiro.lang.codec.OpenBSDBase64 +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.assertArrayEquals import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertTrue class BCryptHashTest { @@ -34,7 +37,7 @@ class BCryptHashTest { @Test void testCreateHashGenerateSaltIterations() { // given - final def testPasswordChars = TEST_PASSWORD.toCharArray(); + final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD) // when final def bCryptHash = BCryptHash.generate testPasswordChars; @@ -46,8 +49,8 @@ class BCryptHashTest { @Test void testCreateHashGivenSalt() { // given - final def testPasswordChars = TEST_PASSWORD.toCharArray(); - final def salt = new SecureRandom().generateSeed 16; + final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD); + final def salt = new SimpleByteSource(new SecureRandom().generateSeed(16)) final def cost = 6 // when @@ -56,7 +59,38 @@ class BCryptHashTest { // then assertEquals cost, bCryptHash.cost; assertEquals pow(2, cost) as int, bCryptHash.iterations; - assertArrayEquals salt, bCryptHash.salt.bytes; + 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/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 4d522faad5..3ad8d6aa84 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 @@ -32,7 +32,7 @@ class ProvidedHashFormatTest { @Test void testDefaults() { def set = ProvidedHashFormat.values() as Set - assertEquals 3, set.size() + assertEquals 4, set.size() assertTrue set.contains(ProvidedHashFormat.HEX) assertTrue set.contains(ProvidedHashFormat.BASE64) assertTrue set.contains(ProvidedHashFormat.SHIRO1) From 6520a493e9aefc5b3ba4fad23ef8ff3ace0e9b06 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Thu, 7 Jan 2021 10:12:22 +0100 Subject: [PATCH 07/11] [SHIRO-290] Add hasher tests - fix invalid cost factor for bcrypt when input is 0. - output Hasher messages using slf4j. --- .../apache/shiro/crypto/hash/BCryptHash.java | 5 +++ .../shiro/crypto/hash/DefaultHashService.java | 15 +++++-- tools/hasher/pom.xml | 6 ++- .../org/apache/shiro/tools/hasher/Hasher.java | 34 +++++++------- tools/hasher/src/main/resources/logback.xml | 31 +++++++++++++ .../apache/shiro/tools/hasher/HasherTest.java | 44 ++++++++++++++++++- .../src/test/resources/logback-test.xml | 28 ++++++++++++ 7 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 tools/hasher/src/main/resources/logback.xml create mode 100644 tools/hasher/src/test/resources/logback-test.xml diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java index 497840447a..dacb5f92d3 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java @@ -71,6 +71,10 @@ protected final void checkValidAlgorithm() { } protected final void checkValidCost() { + checkValidCost(this.cost); + } + + private static void checkValidCost(final int cost) { if (cost < 4 || cost > 31) { final String message = String.format( Locale.ENGLISH, @@ -125,6 +129,7 @@ public static BCryptHash generate(final ByteSource source, final ByteSource init } 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); 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 4b763a168d..c6817349cd 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 @@ -158,7 +158,7 @@ public Hash computeHash(HashRequest request) { String algorithmName = getAlgorithmName(request); ByteSource source = request.getSource(); - int iterations = getIterations(request); + final int iterations = getIterations(request); ByteSource publicSalt = getPublicSalt(request); ByteSource privateSalt = getPrivateSalt(); @@ -170,8 +170,15 @@ public Hash computeHash(HashRequest request) { case "2b": case "2y": // bcrypt + final int cost; + final int iterationsLog2 = (int) (Math.log10(iterations) / Math.log10(2)); + if (iterationsLog2 < 4 || iterationsLog2 > 30) { + cost = 10; + } else { + cost = iterationsLog2; + } ByteSource bcryptSalt = Optional.ofNullable(publicSalt).orElseGet(() -> BCryptHash.createSalt()); - return createBcryptHash(algorithmName, source, bcryptSalt, iterations); + return createBcryptHash(algorithmName, source, bcryptSalt, cost); case "argon2": case "argon2d": case "argon2i": @@ -183,8 +190,8 @@ public Hash computeHash(HashRequest request) { } } - private BCryptHash createBcryptHash(String algorithmName, ByteSource source, ByteSource salt, int iterations) { - return BCryptHash.generate(algorithmName, source, salt, iterations); + private BCryptHash createBcryptHash(String algorithmName, ByteSource source, ByteSource salt, int cost) { + return BCryptHash.generate(algorithmName, source, salt, cost); } private Argon2Hash createArgon2Hash(String algorithmName, ByteSource source, int iterations, ByteSource salt) { diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml index 9af02f84b7..77bc892210 100644 --- a/tools/hasher/pom.xml +++ b/tools/hasher/pom.xml @@ -50,7 +50,11 @@ org.slf4j - slf4j-simple + slf4j-api + + + ch.qos.logback + logback-classic runtime 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 91ee0bad33..f1e04158e1 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 @@ -35,12 +35,14 @@ 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.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; @@ -61,6 +63,8 @@ */ 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; @@ -70,7 +74,7 @@ public final class Hasher { 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)"); @@ -233,10 +237,10 @@ public static void main(String[] args) { 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(); } @@ -250,7 +254,7 @@ public static void main(String[] args) { String output = format.format(hash); - System.out.println(output); + LOG.info(output); } catch (IllegalArgumentException iae) { exit(iae, debug); @@ -341,16 +345,16 @@ private static ByteSource getSalt(String saltString, String saltBytesString, boo 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."); } } } @@ -390,7 +394,7 @@ private static void printHelp(Options options, Exception e, boolean debug) { "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:" + @@ -426,16 +430,16 @@ private static void printHelp(Options options, Exception e, boolean debug) { "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) { diff --git a/tools/hasher/src/main/resources/logback.xml b/tools/hasher/src/main/resources/logback.xml new file mode 100644 index 0000000000..502d9d162f --- /dev/null +++ b/tools/hasher/src/main/resources/logback.xml @@ -0,0 +1,31 @@ + + + + + + + [%-5level] %msg%n + + + + + + + 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 index 5416de8597..79c5ceb117 100644 --- 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 @@ -19,12 +19,21 @@ 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; public class HasherTest { @@ -32,8 +41,14 @@ public class HasherTest { private ByteArrayInputStream testIn; + private final Logger hasherToolLogger = (Logger) LoggerFactory.getLogger("ROOT"); + private final ListAppender listAppender = new ListAppender<>(); + @BeforeEach public void setUpOutput() { + hasherToolLogger.detachAndStopAllAppenders(); + hasherToolLogger.addAppender(listAppender); + listAppender.start(); } private void provideInput(String data) { @@ -42,15 +57,42 @@ private void provideInput(String data) { } @AfterEach - public void restoreSystemInputOutput() { + 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 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 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 0000000000..a6523923f2 --- /dev/null +++ b/tools/hasher/src/test/resources/logback-test.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + From 38b75678a7cfe94b082984f3e2c76cc0f84dcb0b Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Fri, 8 Jan 2021 16:23:47 +0100 Subject: [PATCH 08/11] [SHIRO-290] ServiceLoadable KDF algorithms. - Move BCrypt and Argon2 into their own modules - Add a SPI - Remove hardcoded parameters, replace with ParameterMap for the hashRequest --- core/pom.xml | 8 + .../shiro/authc/SimpleAuthenticationInfo.java | 3 +- .../credential/DefaultPasswordService.java | 6 +- .../credential/HashedCredentialsMatcher.java | 3 +- .../authc/credential/PasswordMatcher.java | 2 +- .../DefaultPasswordServiceTest.groovy | 39 +-- .../credential/PasswordMatcherTest.groovy | 5 +- .../shiro/crypto/hash/AbstractHash.java | 65 ---- .../crypto/hash/ConfigurableHashService.java | 26 +- .../shiro/crypto/hash/DefaultHashService.java | 315 ++---------------- .../shiro/crypto/hash/HashProvider.java | 60 ++++ .../apache/shiro/crypto/hash/HashRequest.java | 72 ++-- .../org/apache/shiro/crypto/hash/HashSpi.java | 48 +++ .../org/apache/shiro/crypto/hash/Md2Hash.java | 3 + .../org/apache/shiro/crypto/hash/Md5Hash.java | 3 + .../apache/shiro/crypto/hash/Sha1Hash.java | 3 + .../shiro/crypto/hash/SimpleHashProvider.java | 224 +++++++++++++ .../shiro/crypto/hash/SimpleHashRequest.java | 40 ++- .../crypto/hash/format/Base64Format.java | 7 +- .../shiro/crypto/hash/format/HashFormat.java | 5 +- .../shiro/crypto/hash/format/HexFormat.java | 9 +- .../crypto/hash/format/ProvidedKdfHashes.java | 82 ----- .../crypto/hash/format/Shiro1CryptFormat.java | 112 ++----- .../crypto/hash/format/Shiro2CryptFormat.java | 4 +- .../hash/src/main/resources/META-INF/NOTICE | 3 - .../org.apache.shiro.crypto.hash.HashSpi | 20 ++ .../crypto/hash/DefaultHashServiceTest.groovy | 82 +---- .../crypto/hash/HashRequestBuilderTest.groovy | 31 +- .../crypto/hash/format/HexFormatTest.groovy | 8 +- crypto/pom.xml | 1 + crypto/support/hashes/argon2/pom.xml | 79 +++++ .../support/hashes/argon2}/Argon2Hash.java | 35 +- .../hashes/argon2/Argon2HashProvider.java | 164 +++++++++ .../argon2/src/main/resources/META-INF/NOTICE | 18 + .../org.apache.shiro.crypto.hash.HashSpi | 20 ++ .../hashes/argon2}/Argon2HashTest.groovy | 2 +- crypto/support/hashes/bcrypt/pom.xml | 79 +++++ .../support/hashes/bcrypt}/BCryptHash.java | 29 +- .../support/hashes/bcrypt/BCryptProvider.java | 146 ++++++++ .../support/hashes/bcrypt}/OpenBSDBase64.java | 2 +- .../bcrypt/src/main/resources/META-INF/NOTICE | 18 + .../org.apache.shiro.crypto.hash.HashSpi | 20 ++ .../hashes/bcrypt}/BCryptHashTest.groovy | 3 +- crypto/support/hashes/pom.xml | 41 +++ crypto/support/pom.xml | 42 +++ pom.xml | 10 + .../org/apache/shiro/tools/hasher/Hasher.java | 1 - 47 files changed, 1231 insertions(+), 767 deletions(-) create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java create mode 100644 crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi create mode 100644 crypto/support/hashes/argon2/pom.xml rename crypto/{hash/src/main/java/org/apache/shiro/crypto/hash => support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2}/Argon2Hash.java (91%) create mode 100644 crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java create mode 100644 crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE create mode 100644 crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi rename crypto/{hash/src/test/groovy/org/apache/shiro/crypto/hash => support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2}/Argon2HashTest.groovy (98%) create mode 100644 crypto/support/hashes/bcrypt/pom.xml rename crypto/{hash/src/main/java/org/apache/shiro/crypto/hash => support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt}/BCryptHash.java (87%) create mode 100644 crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java rename {lang/src/main/java/org/apache/shiro/lang/codec => crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt}/OpenBSDBase64.java (99%) create mode 100644 crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE create mode 100644 crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi rename crypto/{hash/src/test/groovy/org/apache/shiro/crypto/hash => support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt}/BCryptHashTest.groovy (97%) create mode 100644 crypto/support/hashes/pom.xml create mode 100644 crypto/support/pom.xml diff --git a/core/pom.xml b/core/pom.xml index 3c247f1b0a..179b35b3f1 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -101,6 +101,14 @@ org.apache.shiro shiro-crypto-hash + + org.apache.shiro + shiro-crypto-support-hashes-argon2 + + + org.apache.shiro + shiro-crypto-support-hashes-bcrypt + org.apache.shiro shiro-crypto-cipher 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 892b061b27..612d4a904b 100644 --- a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java +++ b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @@ -267,7 +268,7 @@ public boolean equals(Object o) { SimpleAuthenticationInfo that = (SimpleAuthenticationInfo) o; //noinspection RedundantIfStatement - if (principals != null ? !principals.equals(that.principals) : that.principals != null) { + if (!Objects.equals(principals, that.principals)) { return false; } 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 45c8247c00..81d846bebe 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,7 +18,6 @@ */ package org.apache.shiro.authc.credential; -import org.apache.shiro.crypto.hash.Argon2Hash; import org.apache.shiro.crypto.hash.DefaultHashService; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.HashRequest; @@ -49,7 +48,6 @@ public class DefaultPasswordService implements HashingPasswordService { public static final String DEFAULT_HASH_ALGORITHM = "argon2id"; - public static final int DEFAULT_HASH_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS; private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); @@ -63,9 +61,7 @@ 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 Shiro2CryptFormat(); 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 53efb06929..0b53eeed44 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 @@ -439,10 +439,11 @@ private String assertHashAlgorithmName() throws IllegalStateException { * 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(); 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 8e59441a04..d5d489fb5e 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 @@ -55,7 +55,7 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo } //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above): String formatted = (String)storedCredentials; - return passwordService.passwordsMatch(submittedPassword, formatted); + return service.passwordsMatch(submittedPassword, formatted); } private HashingPasswordService assertHashingPasswordService(PasswordService service) { 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 1c97aa26aa..0e370bef96 100644 --- a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy +++ b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy @@ -23,6 +23,7 @@ import org.apache.shiro.crypto.hash.* import org.apache.shiro.crypto.hash.format.HashFormatFactory import org.apache.shiro.crypto.hash.format.HexFormat import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.function.Executable @@ -37,42 +38,42 @@ import static org.junit.jupiter.api.Assertions.* class DefaultPasswordServiceTest { @Test + @DisplayName("throws NPE if plaintext is null") void testEncryptPasswordWithNullArgument() { - def service = new DefaultPasswordService() + def service = createSha256Service() assertThrows(NullPointerException, { service.encryptPassword(null) } as Executable) } @Test void testHashPasswordWithNullArgument() { - def service = new DefaultPasswordService() + def service = createSha256Service() assertNull service.hashPassword(null) } @Test void testEncryptPasswordDefault() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("12345") assertTrue service.passwordsMatch("12345", encrypted) } @Test void testEncryptPasswordWithInvalidMatch() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("ABCDEF") assertFalse service.passwordsMatch("ABC", encrypted) } @Test void testBackwardsCompatibility() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("12345") def submitted = "12345" assertTrue service.passwordsMatch(submitted, encrypted); //change some settings: - service.hashService.hashAlgorithmName = "MD5" - service.hashService.hashIterations = 250000 + service.hashService.defaultAlgorithmName = "MD5" def encrypted2 = service.encryptPassword(submitted) @@ -83,7 +84,7 @@ class DefaultPasswordServiceTest { @Test void testHashFormatWarned() { - def service = new DefaultPasswordService() + def service = createSha256Service() service.hashFormat = new HexFormat() assertTrue service.hashFormat instanceof HexFormat service.encryptPassword("test") @@ -92,7 +93,7 @@ class DefaultPasswordServiceTest { @Test void testPasswordsMatchWithNullOrEmpty() { - def service = new DefaultPasswordService() + def service = createSha256Service() assertTrue service.passwordsMatch(null, (String) null) assertTrue service.passwordsMatch(null, (Hash) null) assertTrue service.passwordsMatch("", (String) null) @@ -142,19 +143,6 @@ class DefaultPasswordServiceTest { 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() { @@ -164,7 +152,7 @@ class DefaultPasswordServiceTest { Locale.setDefault(new Locale("tr", "TR")); try { - PasswordService passwordService = new DefaultPasswordService(); + PasswordService passwordService = createSha256Service() String password = "333"; String enc = passwordService.encryptPassword(password); assertTrue(passwordService.passwordsMatch(password, enc)); @@ -173,4 +161,9 @@ class DefaultPasswordServiceTest { Locale.setDefault(locale); } } + + private static DefaultPasswordService createSha256Service() { + def hashService = new DefaultHashService(defaultAlgorithmName: 'SHA-256') + new DefaultPasswordService(hashService: hashService) + } } diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy index c0d3e7cc41..d900d6feca 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 @@ -25,8 +25,8 @@ 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 java.lang.Math.pow import static org.easymock.EasyMock.* import static org.junit.Assert.* @@ -176,6 +176,7 @@ class PasswordMatcherTest { } @Test + @DisplayName("test whether shiro2 bcrypt password can be parsed and matched.") void testBCryptPassword() { // given def matcher = new PasswordMatcher(); @@ -194,10 +195,10 @@ class PasswordMatcherTest { } @Test + @DisplayName("test whether shiro2 argon2 password can be parsed and matched.") void testArgon2Password() { // given def matcher = new PasswordMatcher(); - def iterations = (int) pow(2, 10) 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' 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 10e071179c..684647c7dc 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 @@ -302,69 +302,4 @@ public int hashCode() { return Arrays.hashCode(this.bytes); } - private static void printMainUsage(Class clazz, String type) { - System.out.println("Prints an " + type + " hash value."); - System.out.println("Usage: java " + clazz.getName() + " [-base64] [-salt ] [-times ] "); - 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 "); - System.out.println("\t-times\t\tHashes the input number of times"); - } - - private static boolean isReserved(String arg) { - return "-base64".equals(arg) || "-times".equals(arg) || "-salt".equals(arg); - } - - @Deprecated - static int doMain(Class 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 fd7883f65f..0aa5701460 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. * @@ -28,21 +25,6 @@ */ public interface ConfigurableHashService extends HashService { - /** - * Sets the 'private' (internal) salt to be paired with a 'public' (random or supplied) salt during hash computation. - * - * @param privateSalt the 'private' internal salt to be paired with a 'public' (random or supplied) salt during - * hash computation. - */ - void setPrivateSalt(ByteSource privateSalt); - - /** - * Sets the number of hash iterations that will be performed during hash computation. - * - * @param iterations the number of hash iterations that will be performed during hash computation. - */ - void setHashIterations(int iterations); - /** * Sets the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute * hashes. @@ -50,12 +32,6 @@ public interface ConfigurableHashService extends HashService { * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to * compute hashes. */ - void setHashAlgorithmName(String name); + void setDefaultAlgorithmName(String name); - /** - * Sets a source of randomness used to generate public salts that will in turn be used during hash computation. - * - * @param rng a source of randomness used to generate public salts that will in turn be used during hash computation. - */ - void setRandomNumberGenerator(RandomNumberGenerator rng); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java index c6817349cd..24281d2b8a 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,42 +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 org.apache.shiro.lang.util.SimpleByteSource; - +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. *

Hash Algorithm

* You may specify a hash algorithm via the {@link #setHashAlgorithmName(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}. - *

Random Salts

- * 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}. - *

Hash Iterations

- * 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 - * blog article - * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'. - *

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

Private Salt

- * If using this implementation as part of a password hashing strategy, it might be desirable to configure a - * {@link #setPrivateSalt(ByteSource) private salt}: - *

+ * will work, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}. + *

* 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). @@ -61,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 * much harder to crack the hash value since the attacker does not have the complete inputs necessary. *

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

- * *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 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 = SimpleByteSource.empty(); - - /** - * 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: *

    - *
  • {@link #setHashAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}
  • - *
  • {@link #setHashIterations(int) hashIterations} = {@code 1}
  • - *
  • {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} = - * new {@link SecureRandomNumberGenerator}()
  • - *
  • {@link #setGeneratePublicSalt(boolean) generatePublicSalt} = {@code false}
  • + *
  • {@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}
  • *
- *

- * 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"; } /** @@ -126,25 +71,10 @@ public DefaultHashService() { *

* A salt will be generated and used to compute the hash. The salt is generated as follows: *

    - *
  1. Use the {@link #getRandomNumberGenerator() randomNumberGenerator} to generate a new random number.
  2. - *
  3. {@link #combine(ByteSource, ByteSource) combine} this random salt with any configured - * {@link #getPrivateSalt() privateSalt} - *
  4. *
  5. Use the combined value as the salt used during hash computation
  6. *
* *
  • - * If the request salt is not null: - *

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

  • - * - *

    - * The returned {@code Hash}'s {@link Hash#getSalt() salt} property - * will contain only the 'public' part of the salt and NOT 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 @@ -157,231 +87,36 @@ public Hash computeHash(HashRequest request) { } String algorithmName = getAlgorithmName(request); - ByteSource source = request.getSource(); - final int iterations = getIterations(request); - ByteSource publicSalt = getPublicSalt(request); - ByteSource privateSalt = getPrivateSalt(); - ByteSource salt = combine(privateSalt, publicSalt); + Optional> kdfHash = HashProvider.getByAlgorithmName(algorithmName); + if (kdfHash.isPresent()) { + HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new); - switch (algorithmName) { - case "2": - case "2a": - case "2b": - case "2y": - // bcrypt - final int cost; - final int iterationsLog2 = (int) (Math.log10(iterations) / Math.log10(2)); - if (iterationsLog2 < 4 || iterationsLog2 > 30) { - cost = 10; - } else { - cost = iterationsLog2; - } - ByteSource bcryptSalt = Optional.ofNullable(publicSalt).orElseGet(() -> BCryptHash.createSalt()); - return createBcryptHash(algorithmName, source, bcryptSalt, cost); - case "argon2": - case "argon2d": - case "argon2i": - case "argon2id": - ByteSource argon2Salt = Optional.ofNullable(publicSalt).orElseGet(() -> Argon2Hash.createSalt()); - return createArgon2Hash(algorithmName, source, iterations, argon2Salt); - default: - return createSimpleHash(algorithmName, source, iterations, publicSalt, salt); + return hashSpi.newHashFactory(random).generate(request); } - } - - private BCryptHash createBcryptHash(String algorithmName, ByteSource source, ByteSource salt, int cost) { - return BCryptHash.generate(algorithmName, source, salt, cost); - } - private Argon2Hash createArgon2Hash(String algorithmName, ByteSource source, int iterations, ByteSource salt) { - return Argon2Hash.generate(algorithmName, source, salt, iterations); + throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName); } - 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 String getAlgorithmName(HashRequest request) { - String name = request.getAlgorithmName(); - if (name == null) { - name = getHashAlgorithmName(); - } - return name; - } - - protected int getIterations(HashRequest request) { - int iterations = Math.max(0, request.getIterations()); - if (iterations < 1) { - iterations = Math.max(1, getHashIterations()); - } - return iterations; - } - - /** - * 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. - *

    - * This implementation functions as follows: - *

      - *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. - *
    3. If the request salt is null or empty: - *
        - *
      1. If a private salt has been set OR {@link #isGeneratePublicSalt()} is {@code true}, - * auto generate a random public salt via the configured - * {@link #getRandomNumberGenerator() randomNumberGenerator}.
      2. - *
      3. 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.
      4. - *
      - *
    4. - *
    - * - * @param request request the request to process - * @return the public salt that should be used to compute a hash based on the specified request or - * {@code null} if no public salt should be used. - */ - protected ByteSource getPublicSalt(HashRequest request) { - - ByteSource publicSalt = request.getSalt(); - - if (publicSalt != null && !publicSalt.isEmpty()) { - //a public salt was explicitly requested to be used - go ahead and use it: - return publicSalt; - } - - publicSalt = null; - - //check to see if we need to generate one: - ByteSource privateSalt = getPrivateSalt(); - boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty(); - - //If a private salt exists, we must generate a public salt to protect the integrity of the private salt. - //Or generate it if the instance is explicitly configured to do so: - if (privateSaltExists || isGeneratePublicSalt()) { - publicSalt = getRandomNumberGenerator().nextBytes(); - } - - return publicSalt; - } - - /** - * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the - * total salt during hash computation. {@code privateSaltBytes} will be {@code null} }if no private salt has been - * configured. - * - * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes - * @param publicSalt the extra bytes to use in addition to the given private salt. - * @return a combination of the specified private salt bytes and extra bytes that will be used as the total - * salt during hash computation. - */ - protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { - - byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null; - int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0; - - byte[] publicSaltBytes = publicSalt != null ? publicSalt.getBytes() : null; - int extraBytesLength = publicSaltBytes != null ? publicSaltBytes.length : 0; - - int length = privateSaltLength + extraBytesLength; - - if (length <= 0) { - return SimpleByteSource.empty(); - } - - 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); + return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName); } @Override - public void setHashAlgorithmName(String name) { - this.algorithmName = name; + public void setDefaultAlgorithmName(String name) { + this.defaultAlgorithmName = name; } - public String getHashAlgorithmName() { - return this.algorithmName; - } - - @Override - public void setPrivateSalt(ByteSource privateSalt) { - this.privateSalt = privateSalt; + public String getDefaultAlgorithmName() { + return this.defaultAlgorithmName; } - public ByteSource getPrivateSalt() { - return this.privateSalt; + public Random getRandom() { + return random; } - @Override - public void setHashIterations(int count) { - this.iterations = count; - } - - public int getHashIterations() { - return this.iterations; - } - - @Override - 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. - *

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

    - * NOTE: 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. - *

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

    - * NOTE: 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; + public void setRandom(Random random) { + this.random = random; } } 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 0000000000..84f90ce680 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java @@ -0,0 +1,60 @@ +/* + * 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. + * + *

    Instead of maintaining them as an {@code Enum}, ServiceLoaders would provide a pluggable alternative.

    + */ +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> getByAlgorithmName(String algorithmName) { + requireNonNull(algorithmName, "algorithmName in HashProvider.getByAlgorithmName"); + ServiceLoader> hashSpis = load(); + + return StreamSupport.stream(hashSpis.spliterator(), false) + .filter(hashSpi -> hashSpi.getImplementedAlgorithms().contains(algorithmName)) + .findAny(); + } + + @SuppressWarnings("unchecked") + private static ServiceLoader> load() { + return (ServiceLoader>) (Object) 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 b66c79ac86..2f0232c1c1 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 @@ -21,6 +21,12 @@ 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 * 'digest'). While you can instantiate a concrete {@code HashRequest} class directly, most will find using the @@ -50,19 +56,7 @@ public interface HashRequest { * @return a salt to be used by the {@link HashService} during hash computation, or {@code null} if no salt is * provided as part of the request. */ - ByteSource getSalt(); - - /** - * Returns the number of requested hash iterations to be performed when computing the final {@code Hash} result. - * A non-positive (0 or less) indicates that the {@code HashService}'s default iteration configuration should - * be used. A positive value overrides the {@code HashService}'s configuration for a single request. - *

    - * 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 getSalt(); /** * Returns the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or @@ -73,9 +67,26 @@ public interface HashRequest { * sufficient to meet a desired level of security. * * @return the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or - * {@code null} if the default algorithm configuration of the {@code HashService} should be used. + * {@code null} if the default algorithm configuration of the {@code HashService} should be used. + */ + Optional getAlgorithmName(); + + /** + * Returns various parameters for the requested hash. + * + *

    If the map is empty for a specific parameter, the implementation must select the default.

    + * + *

    Implementations should provide a nested {@code .Parameters} class with {@code public static final String}s + * for convenience.

    + * + *

    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.

    + * + * @return the parameters for the requested hash to be used when computing the final {@code Hash} result. + * @throws NullPointerException if any of the values is {@code null}. */ - String getAlgorithmName(); + Map getParameters(); /** * A Builder class representing the Builder design pattern for constructing {@link HashRequest} instances. @@ -87,14 +98,13 @@ public static class Builder { private ByteSource source; private ByteSource salt = SimpleByteSource.empty(); - private int iterations; + private Map parameters = new ConcurrentHashMap<>(); private String algorithmName; /** * Default no-arg constructor. */ public Builder() { - this.iterations = 0; } /** @@ -171,24 +181,14 @@ public Builder setSalt(Object salt) throws IllegalArgumentException { 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. - *

    - * 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 parameters) { + this.parameters.clear(); + this.parameters.putAll(requireNonNull(parameters)); return this; } @@ -220,7 +220,7 @@ public Builder setAlgorithmName(String algorithmName) { * @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 0000000000..f9bd8f6cb7 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java @@ -0,0 +1,48 @@ +/* + * 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; + +public interface HashSpi { + + Class getImplementationClass(); + + Set getImplementedAlgorithms(); + + T fromString(String format); + + /** + * A factory class for the hash of the type {@code }. + * + *

    Implementations are highly encouraged to use the given random parameter as + * source of random bytes (e.g. for seeds).

    + * + * @param random a source of {@link Random}, usually {@code SecureRandom}. + * @return a factory class for creating instances of {@code }. + */ + HashFactory newHashFactory(Random random); + + interface HashFactory { + + T 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 index dbfb9cb3db..a5e4d5b683 100644 --- 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 @@ -30,10 +30,13 @@ * techniques and how the overloaded constructors function. * * @since 0.9 + * @deprecated and will throw exceptions since 2.0.0, to be removed in 2.1.0. */ +@Deprecated public class Md2Hash extends SimpleHash { public static final String ALGORITHM_NAME = "MD2"; + private static final long serialVersionUID = 3432560796303546217L; public Md2Hash() { super(ALGORITHM_NAME); 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 index a83740a642..6cbb12699a 100644 --- 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 @@ -29,12 +29,15 @@ * techniques and how the overloaded constructors function. * * @since 0.9 + * @deprecated since 2.0.0, will throw exceptions in 2.1.0 and to be removed in 2.2.0. */ +@Deprecated public class Md5Hash extends SimpleHash { //TODO - complete JavaDoc public static final String ALGORITHM_NAME = "MD5"; + private static final long serialVersionUID = -1297357676456275955L; public Md5Hash() { super(ALGORITHM_NAME); 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 index e844b70408..e32860666b 100644 --- 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 @@ -30,12 +30,15 @@ * techniques and how the overloaded constructors function. * * @since 0.9 + * @deprecated since 2.0.0, will throw exceptions in 2.1.0 and to be removed in 2.2.0. */ +@Deprecated public class Sha1Hash extends SimpleHash { //TODO - complete JavaDoc public static final String ALGORITHM_NAME = "SHA-1"; + private static final long serialVersionUID = 3437720230699638519L; public Sha1Hash() { super(ALGORITHM_NAME); 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 0000000000..f916716f39 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java @@ -0,0 +1,224 @@ +/* + * 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; + +public class SimpleHashProvider implements HashSpi { + + private static final Set IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[]{ + Md2Hash.ALGORITHM_NAME, + Md5Hash.ALGORITHM_NAME, + Sha1Hash.ALGORITHM_NAME, + Sha256Hash.ALGORITHM_NAME, + Sha384Hash.ALGORITHM_NAME, + Sha512Hash.ALGORITHM_NAME + }) + .collect(toSet()); + + @Override + public Class getImplementationClass() { + return SimpleHash.class; + } + + @Override + public Set 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); + } + + private ByteSource getSecretSalt(HashRequest request) { + Optional 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; + } + + /** + * 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. + *

    + * This implementation functions as follows: + *

      + *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. + *
    3. If the request salt is null or empty: + *
        + *
      + *
    4. + *
    + * + * @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 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); + } + + /** + * 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"}. + * + *

    For more information, see Pepper (cryptography) on Wikipedia.

    + */ + 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 5c3101ae58..ffd2989d28 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,11 @@ 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; /** @@ -31,28 +36,26 @@ public class SimpleHashRequest implements HashRequest { private final ByteSource source; //cannot be null - this is the source to hash. private final ByteSource salt; //null = no salt specified - private final int iterations; //0 = not specified by the requestor; let the HashService decide. private final String algorithmName; //null = let the HashService decide. + private final Map 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) { + public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, Map parameters) { this.source = requireNonNull(source); this.salt = salt; this.algorithmName = algorithmName; - this.iterations = Math.max(0, iterations); + this.parameters.putAll(requireNonNull(parameters)); } @Override @@ -61,17 +64,18 @@ public ByteSource getSource() { } @Override - public ByteSource getSalt() { - return this.salt; + public Optional getSalt() { + return Optional.ofNullable(this.salt); } + @Override - public int getIterations() { - return iterations; + public Optional getAlgorithmName() { + return Optional.ofNullable(algorithmName); } @Override - public String getAlgorithmName() { - return algorithmName; + public Map 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 022c3067ef..35b3394b3c 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 @@ -28,14 +28,17 @@ * 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}. */ @Override public String format(final Hash hash) { 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 c65ae78b5f..29d8535f95 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. *

    * Note that not all HashFormat algorithms are reversible. That is, they can't be parsed and reconstituted to the - * original Hash instance. The traditional - * Unix crypt(3) is one such format. + * original Hash instance. *

    * The formats that are reversible however will be represented as {@link ParsableHashFormat} instances. * * @see ParsableHashFormat - * * @since 1.2 */ public interface HashFormat { @@ -40,6 +38,7 @@ public interface HashFormat { * * @param hash the hash instance to format into a String. * @return a formatted string representing the specified Hash instance. + * @throws NullPointerException if given parameter hash is {@code null}. */ String format(Hash hash); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java index ad30293434..2dfb802819 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,17 +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}. */ @Override public String format(final Hash hash) { - return hash != null ? hash.toHex() : null; + return hash.toHex(); } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java deleted file mode 100644 index 09736abf9a..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedKdfHashes.java +++ /dev/null @@ -1,82 +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.format; - -import org.apache.shiro.crypto.hash.AbstractCryptHash; -import org.apache.shiro.crypto.hash.Argon2Hash; -import org.apache.shiro.crypto.hash.BCryptHash; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.function.Function; - -import static java.util.Collections.unmodifiableList; - -/** - * Hashes used by the Shiro2CryptFormat class. - */ -public enum ProvidedKdfHashes { - ARGON2( - new String[]{"argon2id", "argon2i", "argon2d"}, - Argon2Hash::fromString - ), - BCRYPT( - new String[]{"2", "2y", "2a", "2b"}, - BCryptHash::fromString - ); - - private final List recognizedAlgorithms; - - private final Function fromStringMethod; - - ProvidedKdfHashes(String[] recognizedAlgorithms, Function fromStringMethod) { - this.recognizedAlgorithms = unmodifiableList(Arrays.asList(recognizedAlgorithms)); - this.fromStringMethod = fromStringMethod; - } - - public static Optional getByAlgorithmName(String algorithmName) { - return Arrays.stream(values()) - .filter(val -> val.getRecognizedAlgorithms().contains(algorithmName)) - .findAny(); - } - - public List getRecognizedAlgorithms() { - return recognizedAlgorithms; - } - - public Function getFromStringMethod() { - return fromStringMethod; - } - - public AbstractCryptHash fromString(String input) { - return getFromStringMethod().apply(input); - } - - @Override - public String toString() { - return new StringJoiner(", ", ProvidedKdfHashes.class.getSimpleName() + "[", "]") - .add("super=" + super.toString()) - .add("recognizedAlgorithms=" + recognizedAlgorithms) - .add("fromStringMethod=" + fromStringMethod) - .toString(); - } -} 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 0e6bae451d..1428f3abd9 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,19 +18,13 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.crypto.hash.Argon2Hash; -import org.apache.shiro.crypto.hash.BCryptHash; 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.codec.OpenBSDBase64; import org.apache.shiro.lang.util.ByteSource; -import org.apache.shiro.lang.util.SimpleByteSource; import org.apache.shiro.lang.util.StringUtils; -import java.nio.charset.StandardCharsets; -import java.util.Locale; - /** * The {@code Shiro1CryptFormat} is a fully reversible * Modular Crypt Format (MCF). Because it is @@ -70,8 +64,7 @@ * * 3 * {@code iterationCount} - * The number of hash iterations performed. In case of argon2, this is a comma separated list, - * containing the number of iterations, the memory in kiB and the parallelism count. + * The number of hash iterations performed. * true (1 <= N <= Integer.MAX_VALUE) * * @@ -112,11 +105,10 @@ public String format(final Hash hash) { return null; } - final String algorithmName = hash.getAlgorithmName(); - final ByteSource salt = hash.getSalt(); - String iterationParameter = formatIterationParameter(hash); - - final StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterationParameter).append(TOKEN_DELIMITER); + String algorithmName = hash.getAlgorithmName(); + ByteSource salt = hash.getSalt(); + int iterations = hash.getIterations(); + StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); if (salt != null) { sb.append(salt.toBase64()); @@ -128,22 +120,6 @@ public String format(final Hash hash) { return sb.toString(); } - private String formatIterationParameter(Hash hash) { - if (hash instanceof Argon2Hash) { - Argon2Hash argon2Hash = (Argon2Hash) hash; - - return String.format( - Locale.ENGLISH, - "%d,%d,%d", - hash.getIterations(), - argon2Hash.getMemoryKiB(), - argon2Hash.getParallelism() - ); - } - - return "" + hash.getIterations(); - } - @Override public Hash parse(final String formatted) { if (formatted == null) { @@ -151,78 +127,48 @@ public Hash parse(final String formatted) { } if (!formatted.startsWith(MCF_PREFIX)) { //TODO create a HashFormatException class - final String msg = "The argument is not a valid '" + ID + "' formatted hash."; + 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("\\$"); + 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; - final String digestBase64 = parts[i--]; + String digestBase64 = parts[i--]; //second-to-last part is always the salt, Base64-encoded: - final String saltBase64 = parts[i--]; - final String iterationsString = parts[i--]; - final String algorithmName = parts[0]; + String saltBase64 = parts[i--]; + String iterationsString = parts[i--]; - switch (algorithmName) { - case "2": - case "2a": - case "2b": - case "2y": - // bcrypt - throw new UnsupportedOperationException("bcrypt is not supported in shiro1 format."); - case "argon2": - case "argon2d": - case "argon2i": - case "argon2id": - // argon2 - throw new UnsupportedOperationException("argon2 is not supported in shiro1 format."); - default: - // continue parsing - } + byte[] digest = Base64.decode(digestBase64); + ByteSource salt = null; - final byte[] digest; - if (BCryptHash.getAlgorithmsBcrypt().contains(algorithmName)) { - digest = new OpenBSDBase64.Default().decode(digestBase64.getBytes(StandardCharsets.ISO_8859_1)); - } else { - digest = Base64.decode(digestBase64); + if (StringUtils.hasLength(saltBase64)) { + byte[] saltBytes = Base64.decode(saltBase64); + salt = ByteSource.Util.bytes(saltBytes); } - ByteSource salt = parseSalt(saltBase64, algorithmName); - final int iterations; + int iterations; try { iterations = Integer.parseInt(iterationsString); - } catch (final NumberFormatException e) { - final String msg = "Unable to parse formatted hash string: " + formatted; + } catch (NumberFormatException e) { + String msg = "Unable to parse formatted hash string: " + formatted; throw new IllegalArgumentException(msg, e); } - final SimpleHash hash = new SimpleHash(algorithmName); + SimpleHash hash = new SimpleHash(algorithmName); hash.setBytes(digest); - hash.setSalt(salt); + if (salt != null) { + hash.setSalt(salt); + } hash.setIterations(iterations); return hash; } - - private ByteSource parseSalt(String base64, String algorithmName) { - if (!StringUtils.hasLength(base64)) { - return SimpleByteSource.empty(); - } - - switch (algorithmName) { - case "2": - case "2a": - case "2b": - case "2y": - byte[] saltBytesBcrypt = new OpenBSDBase64.Default().decode(base64.getBytes(StandardCharsets.ISO_8859_1)); - return new SimpleByteSource(saltBytesBcrypt); - default: - final byte[] saltBytes = Base64.decode(base64); - return new SimpleByteSource(saltBytes); - - } - } } 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 index 5e16b664cd..d0365e3254 100644 --- 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 @@ -20,6 +20,8 @@ import org.apache.shiro.crypto.hash.AbstractCryptHash; import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.HashProvider; +import org.apache.shiro.crypto.hash.HashSpi; import org.apache.shiro.crypto.hash.SimpleHash; import static java.util.Objects.requireNonNull; @@ -127,7 +129,7 @@ public Hash parse(final String formatted) { final String[] parts = suffix.split("\\$"); final String algorithmName = parts[0]; - ProvidedKdfHashes kdfHash = ProvidedKdfHashes.getByAlgorithmName(algorithmName) + 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 29ce9b121c..5976d79428 100644 --- a/crypto/hash/src/main/resources/META-INF/NOTICE +++ b/crypto/hash/src/main/resources/META-INF/NOTICE @@ -9,9 +9,6 @@ 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 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 0000000000..dc9d0d2317 --- /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 d021be272c..389ba3a140 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy @@ -18,12 +18,10 @@ */ package org.apache.shiro.crypto.hash -import org.apache.shiro.crypto.RandomNumberGenerator -import org.apache.shiro.crypto.SecureRandomNumberGenerator + import org.apache.shiro.lang.util.ByteSource import org.junit.Test -import static org.easymock.EasyMock.* import static org.junit.Assert.* /** @@ -35,54 +33,27 @@ class DefaultHashServiceTest { @Test void testNullRequest() { - assertNull createService().computeHash(null) + assertNull createSha256Service().computeHash(null) } @Test void testDifferentAlgorithmName() { - def service = new DefaultHashService(hashAlgorithmName: 'MD5') - def hash = hash(service, "test") - assertEquals 'MD5', hash.algorithmName - } + // given + def newAlgorithm = 'SHA-512' + def service = new DefaultHashService(defaultAlgorithmName: newAlgorithm) - @Test - void testDifferentIterations() { - def service = new DefaultHashService(hashIterations: 2) + // when def hash = hash(service, "test") - assertEquals 2, hash.iterations - } - - @Test - void testDifferentRandomNumberGenerator() { - def ByteSource randomBytes = new SecureRandomNumberGenerator().nextBytes() - def rng = createMock(RandomNumberGenerator) - expect(rng.nextBytes()).andReturn randomBytes - - replay rng - - def service = new DefaultHashService(randomNumberGenerator: rng, generatePublicSalt: true) - hash(service, "test") - - verify rng - } - - /** - * If 'generatePublicSalt' is true, 2 hashes of the same input source should be different. - */ - @Test - void testWithRandomlyGeneratedSalt() { - def service = new DefaultHashService(generatePublicSalt: true) - def first = hash(service, "password") - def second = hash(service, "password") - assertFalse first == second + // then + assertEquals newAlgorithm, hash.algorithmName } @Test void testRequestWithEmptySource() { def source = ByteSource.Util.bytes((byte[])null) def request = new HashRequest.Builder().setSource(source).build() - def service = createService() + def service = createSha256Service() assertNull service.computeHash(request) } @@ -92,7 +63,7 @@ class DefaultHashServiceTest { */ @Test void testOnlyRandomSaltHash() { - HashService service = createService(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password2", first.salt); assertFalse first == second @@ -104,7 +75,7 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsRandomness() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password"); assertFalse first == second @@ -117,7 +88,7 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsReturn() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password", first.salt); assertEquals first, second @@ -129,24 +100,12 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsHash() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password2", first.salt); assertFalse first == second } - /** - * Hash result is different if the base salt is added. - */ - @Test - public void testPrivateSaltChangesResult() { - HashService saltedService = createServiceWithPrivateSalt(); - HashService service = createService(); - Hash first = hashPredictable(saltedService, "password"); - Hash second = hashPredictable(service, "password"); - assertFalse first == second - } - protected Hash hash(HashService hashService, def source) { return hashService.computeHash(new HashRequest.Builder().setSource(source).build()); } @@ -155,19 +114,8 @@ class DefaultHashServiceTest { return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build()); } - private Hash hashPredictable(HashService hashService, def source) { - byte[] salt = new byte[20]; - Arrays.fill(salt, (byte) 2); - return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build()); - } - - private DefaultHashService createService() { - return new DefaultHashService(); + private static DefaultHashService createSha256Service() { + return new DefaultHashService(defaultAlgorithmName: 'SHA-256'); } - private DefaultHashService createServiceWithPrivateSalt() { - DefaultHashService defaultHashService = new DefaultHashService(); - defaultHashService.setPrivateSalt(new SecureRandomNumberGenerator().nextBytes()); - return defaultHashService; - } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy index 527098cb30..323de38fa9 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy @@ -20,9 +20,10 @@ package org.apache.shiro.crypto.hash import org.apache.shiro.crypto.SecureRandomNumberGenerator import org.apache.shiro.lang.util.ByteSource -import org.junit.Test +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.* -import static org.junit.Assert.* /** * Unit tests for the {@link HashRequest.Builder} implementation @@ -33,16 +34,7 @@ class HashRequestBuilderTest { @Test void testNullSource() { - try { - new HashRequest.Builder().build() - fail "NullPointerException should be thrown" - } catch (NullPointerException expected) { - } - } - - @Test - void testDefault() { - assertEquals 0, new HashRequest.Builder().setSource("test").build().iterations + assertThrows NullPointerException, { new HashRequest.Builder().build() } } @Test @@ -50,15 +42,16 @@ class HashRequestBuilderTest { ByteSource source = ByteSource.Util.bytes("test") ByteSource salt = new SecureRandomNumberGenerator().nextBytes() def request = new HashRequest.Builder() - .setSource(source) - .setSalt(salt) - .setIterations(2) - .setAlgorithmName('MD5').build() + .setSource(source) + .setSalt(salt) + .addParameter(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS, 2) + .setAlgorithmName('MD5') + .build() assertNotNull request assertEquals source, request.source - assertEquals salt, request.salt - assertEquals 2, request.iterations - assertEquals 'MD5', request.algorithmName + assertEquals salt, request.salt.orElse(null) + assertEquals 2, request.getParameters().get(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS) + assertEquals 'MD5', request.algorithmName.orElse(null) } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy index de71cc1428..30f18aa8e6 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 @@ -20,8 +20,10 @@ 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.junit.jupiter.api.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertThrows /** * Unit tests for the {@link HexFormat} implementation. @@ -41,7 +43,7 @@ class HexFormatTest { @Test void testFormatWithNullArgument() { HexFormat format = new HexFormat() - assertNull format.format(null) + assertThrows NullPointerException, { format.format(null) } } } diff --git a/crypto/pom.xml b/crypto/pom.xml index b7f0e680c2..72dba2b027 100644 --- a/crypto/pom.xml +++ b/crypto/pom.xml @@ -36,6 +36,7 @@ core hash cipher + support diff --git a/crypto/support/hashes/argon2/pom.xml b/crypto/support/hashes/argon2/pom.xml new file mode 100644 index 0000000000..2a3a0f9a04 --- /dev/null +++ b/crypto/support/hashes/argon2/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + + org.apache.shiro + shiro-hashes + 2.0.0-SNAPSHOT + ../pom.xml + + + shiro-crypto-support-hashes-argon2 + Apache Shiro :: Cryptography :: Support :: Hashes :: Argon2 + + bundle + + + + org.apache.shiro + shiro-crypto-hash + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.shiro.hashes.argon2 + org.apache.hashes.argon2*;version=${project.version} + + org.apache.shiro*;version="${shiro.osgi.importRange}", + org.aopalliance*;version="[1.0.0, 2.0.0)", + com.google.inject*;version="1.3", + * + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java similarity index 91% rename from crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java rename to crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java index c6f10aad3f..9dd1f47dee 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Argon2Hash.java +++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java @@ -17,8 +17,9 @@ * under the License. */ -package org.apache.shiro.crypto.hash; +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; @@ -28,13 +29,14 @@ import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64.Encoder; -import java.util.List; +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.unmodifiableList; +import static java.util.Collections.unmodifiableSet; import static java.util.Objects.requireNonNull; /** @@ -52,21 +54,22 @@ public class Argon2Hash extends AbstractCryptHash { private static final long serialVersionUID = 2647354947284558921L; - private static final Pattern DELIMITER_COMMA = Pattern.compile(","); - - private static final String ALGORITHM_NAME = "argon2id"; + public static final String DEFAULT_ALGORITHM_NAME = "argon2id"; - private static final int DEFAULT_VERSION = Argon2Parameters.ARGON2_VERSION_13; + public static final int DEFAULT_ALGORITHM_VERSION = Argon2Parameters.ARGON2_VERSION_13; public static final int DEFAULT_ITERATIONS = 3; - private static final List ALGORITHMS_ARGON2 = Arrays.asList("argon2id", "argon2i", "argon2d"); + public static final int DEFAULT_MEMORY_KIB = 4096; + + private static final Set ALGORITHMS_ARGON2 = new HashSet<>(Arrays.asList("argon2id", "argon2i", "argon2d")); + + private static final Pattern DELIMITER_COMMA = Pattern.compile(","); - private static final int DEFAULT_MEMORY_POW_2 = 4096; - private static final int DEFAULT_PARALLELISM = 4; + public static final int DEFAULT_PARALLELISM = 4; - private static final int DEFAULT_OUTPUT_LENGTH = 32; + public static final int DEFAULT_OUTPUT_LENGTH = 32; /** @@ -92,8 +95,8 @@ public Argon2Hash(String algorithmName, int argonVersion, byte[] hashedData, Byt checkValidIterations(); } - public static List getAlgorithmsArgon2() { - return unmodifiableList(ALGORITHMS_ARGON2); + public static Set getAlgorithmsArgon2() { + return unmodifiableSet(ALGORITHMS_ARGON2); } public static ByteSource createSalt() { @@ -107,7 +110,7 @@ public static Argon2Hash fromString(String input) { throw new UnsupportedOperationException("Unsupported input: " + input); } - final String[] parts = DELIMITER.split(input.substring(1)); + final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1)); final String algorithmName = parts[0].trim(); if (!ALGORITHMS_ARGON2.contains(algorithmName)) { @@ -164,11 +167,11 @@ public static Argon2Hash generate(final char[] source) { } public static Argon2Hash generate(final ByteSource source, final ByteSource salt, final int iterations) { - return generate(ALGORITHM_NAME, source, requireNonNull(salt, "salt"), 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_VERSION, source, salt, iterations, DEFAULT_MEMORY_POW_2, DEFAULT_PARALLELISM, DEFAULT_OUTPUT_LENGTH); + return generate(algorithmName, DEFAULT_ALGORITHM_VERSION, source, salt, iterations, DEFAULT_MEMORY_KIB, DEFAULT_PARALLELISM, DEFAULT_OUTPUT_LENGTH); } public static Argon2Hash generate( 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 0000000000..90a669f1c7 --- /dev/null +++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java @@ -0,0 +1,164 @@ +/* + * 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; + +public class Argon2HashProvider implements HashSpi { + + private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class); + + @Override + public Class getImplementationClass() { + return Argon2Hash.class; + } + + @Override + public Set getImplementedAlgorithms() { + return Argon2Hash.getAlgorithmsArgon2(); + } + + @Override + public Argon2Hash fromString(String format) { + return Argon2Hash.fromString(format); + } + + @Override + public HashFactory newHashFactory(Random random) { + return new Argon2HashFactory(random); + } + + static class Argon2HashFactory implements HashSpi.HashFactory { + + private final SecureRandom random; + + public Argon2HashFactory(Random random) { + if (!(random instanceof SecureRandom)) { + throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!"); + } + + this.random = (SecureRandom) random; + } + + @Override + public Argon2Hash generate(HashRequest hashRequest) { + final String algorithmName = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_NAME)) + .map(algo -> (String) algo) + .orElse(Parameters.DEFAULT_ALGORITHM_NAME); + + final int version = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_VERSION)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ALGORITHM_VERSION)) + .orElse(Parameters.DEFAULT_ALGORITHM_VERSION); + + final ByteSource salt = parseSalt(hashRequest); + + final int iterations = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ITERATIONS)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ITERATIONS)) + .orElse(Parameters.DEFAULT_ITERATIONS); + + final int memoryKib = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_MEMORY_KIB)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_MEMORY_KIB)) + .orElse(Parameters.DEFAULT_MEMORY_KIB); + + final int parallelism = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_PARALLELISM)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_PARALLELISM)) + .orElse(Parameters.DEFAULT_PARALLELISM); + + final int outputLength = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_OUTPUT_LENGTH)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_OUTPUT_LENGTH)) + .orElse(Parameters.DEFAULT_OUTPUT_LENGTH); + + return Argon2Hash.generate( + algorithmName, + version, + hashRequest.getSource(), + salt, + iterations, + memoryKib, + parallelism, + outputLength + ); + } + + private ByteSource parseSalt(HashRequest hashRequest) { + return Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_SALT)) + .map(saltParm -> Base64.getDecoder().decode((String) saltParm)) + .map(SimpleByteSource::new) + .flatMap(this::lengthValidOrEmpty) + .orElseGet(Argon2Hash::createSalt); + } + + private Optional lengthValidOrEmpty(ByteSource bytes) { + if (bytes.getBytes().length != 16) { + return Optional.empty(); + } + + return Optional.of(bytes); + } + + private Optional 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(); + } + } + } + + static final class Parameters { + + public static final String DEFAULT_ALGORITHM_NAME = Argon2Hash.DEFAULT_ALGORITHM_NAME; + public static final int DEFAULT_ALGORITHM_VERSION = Argon2Hash.DEFAULT_ALGORITHM_VERSION; + public static final int DEFAULT_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS; + public static final int DEFAULT_MEMORY_KIB = Argon2Hash.DEFAULT_MEMORY_KIB; + public static final int DEFAULT_PARALLELISM = Argon2Hash.DEFAULT_PARALLELISM; + public static final int DEFAULT_OUTPUT_LENGTH = Argon2Hash.DEFAULT_OUTPUT_LENGTH; + + public static final String PARAMETER_ALGORITHM_NAME = "Argon2.algorithmName"; + public static final String PARAMETER_ALGORITHM_VERSION = "Argon2.version"; + 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"; + public static final String PARAMETER_OUTPUT_LENGTH = "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 0000000000..4b3b138f99 --- /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 0000000000..80a9e65fec --- /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/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy similarity index 98% rename from crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy rename to crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy index 8f9e305fe1..638fa8dd4a 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/Argon2HashTest.groovy +++ b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.shiro.crypto.hash +package org.apache.shiro.crypto.support.hashes.argon2 import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat diff --git a/crypto/support/hashes/bcrypt/pom.xml b/crypto/support/hashes/bcrypt/pom.xml new file mode 100644 index 0000000000..9b9cc78306 --- /dev/null +++ b/crypto/support/hashes/bcrypt/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + + org.apache.shiro + shiro-hashes + 2.0.0-SNAPSHOT + ../pom.xml + + + shiro-crypto-support-hashes-bcrypt + Apache Shiro :: Cryptography :: Support :: Hashes :: BCrypt + + bundle + + + + org.apache.shiro + shiro-crypto-hash + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.shiro.hashes.bcrypt + org.apache.hashes.bcrypt*;version=${project.version} + + org.apache.shiro*;version="${shiro.osgi.importRange}", + org.aopalliance*;version="[1.0.0, 2.0.0)", + com.google.inject*;version="1.3", + * + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java similarity index 87% rename from crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java rename to crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java index dacb5f92d3..028b3c38f2 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/BCryptHash.java +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java @@ -17,9 +17,9 @@ * under the License. */ -package org.apache.shiro.crypto.hash; +package org.apache.shiro.crypto.support.hashes.bcrypt; -import org.apache.shiro.lang.codec.OpenBSDBase64; +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; @@ -27,23 +27,24 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; -import java.util.List; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import java.util.StringJoiner; -import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; public class BCryptHash extends AbstractCryptHash { private static final long serialVersionUID = 6957869292324606101L; - protected static final int DEFAULT_COST = 10; + public static final String DEFAULT_ALGORITHM_NAME = "2y"; - private static final String ALGORITHM_NAME = "2y"; + public static final int DEFAULT_COST = 10; - private static final int SALT_LENGTH = 16; + public static final int SALT_LENGTH = 16; - private static final List ALGORITHMS_BCRYPT = Arrays.asList("2", "2a", "2b", "2y"); + private static final Set ALGORITHMS_BCRYPT = new HashSet<>(Arrays.asList("2", "2a", "2b", "2y")); private final int cost; @@ -74,7 +75,7 @@ protected final void checkValidCost() { checkValidCost(this.cost); } - private static void checkValidCost(final int cost) { + public static int checkValidCost(final int cost) { if (cost < 4 || cost > 31) { final String message = String.format( Locale.ENGLISH, @@ -83,14 +84,16 @@ private static void checkValidCost(final int cost) { ); throw new IllegalArgumentException(message); } + + return cost; } public int getCost() { return this.cost; } - public static List getAlgorithmsBcrypt() { - return unmodifiableList(ALGORITHMS_BCRYPT); + public static Set getAlgorithmsBcrypt() { + return unmodifiableSet(ALGORITHMS_BCRYPT); } public static BCryptHash fromString(String input) { @@ -100,7 +103,7 @@ public static BCryptHash fromString(String input) { throw new UnsupportedOperationException("Unsupported input: " + input); } - final String[] parts = DELIMITER.split(input.substring(1)); + 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) + "'."); @@ -125,7 +128,7 @@ public static BCryptHash generate(final ByteSource source) { public static BCryptHash generate(final ByteSource source, final ByteSource initialSalt, final int cost) { - return generate(ALGORITHM_NAME, source, initialSalt, cost); + return generate(DEFAULT_ALGORITHM_NAME, source, initialSalt, cost); } public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) { 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 0000000000..0c76ff8a02 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java @@ -0,0 +1,146 @@ +/* + * 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; + +public class BCryptProvider implements HashSpi { + + private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.class); + + @Override + public Class getImplementationClass() { + return BCryptHash.class; + } + + @Override + public Set 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 parameters = hashRequest.getParameters(); + final Optional 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 parameters = hashRequest.getParameters(); + final Optional optSaltBase64 = Optional.ofNullable(parameters.get(Parameters.PARAMETER_SALT)) + .map(obj -> (String) obj); + + if (!optSaltBase64.isPresent()) { + return BCryptHash.createSalt(); + } + + final String saltBase64 = optSaltBase64.orElseThrow(NoSuchElementException::new); + final byte[] saltBytes = Base64.getDecoder().decode(saltBase64); + + if (saltBytes.length != BCryptHash.SALT_LENGTH) { + return BCryptHash.createSalt(); + } + + return new SimpleByteSource(saltBytes); + } + } + + 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/lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java similarity index 99% rename from lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java rename to crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java index c6184e67af..531fa99f2c 100644 --- a/lang/src/main/java/org/apache/shiro/lang/codec/OpenBSDBase64.java +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.shiro.lang.codec; +package org.apache.shiro.crypto.support.hashes.bcrypt; /** 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 0000000000..4b3b138f99 --- /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 0000000000..95d1df35c6 --- /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/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy similarity index 97% rename from crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy rename to crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy index 93520582a1..60415a0883 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/BCryptHashTest.groovy +++ b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy @@ -17,9 +17,8 @@ * under the License. */ -package org.apache.shiro.crypto.hash +package org.apache.shiro.crypto.support.hashes.bcrypt -import org.apache.shiro.lang.codec.OpenBSDBase64 import org.apache.shiro.lang.util.SimpleByteSource import org.junit.jupiter.api.Test diff --git a/crypto/support/hashes/pom.xml b/crypto/support/hashes/pom.xml new file mode 100644 index 0000000000..6cea07c57a --- /dev/null +++ b/crypto/support/hashes/pom.xml @@ -0,0 +1,41 @@ + + + + + + org.apache.shiro + shiro-support + 2.0.0-SNAPSHOT + ../../../support/pom.xml + + + 4.0.0 + shiro-hashes + Apache Shiro :: Cryptography :: Support :: Hashes + pom + + + bcrypt + argon2 + + + diff --git a/crypto/support/pom.xml b/crypto/support/pom.xml new file mode 100644 index 0000000000..87aa859f80 --- /dev/null +++ b/crypto/support/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + + org.apache.shiro + shiro-crypto + 2.0.0-SNAPSHOT + ../pom.xml + + + support + Apache Shiro :: Cryptography :: Support + pom + + + hashes + + + + diff --git a/pom.xml b/pom.xml index e7e580a094..20739888a4 100644 --- a/pom.xml +++ b/pom.xml @@ -751,6 +751,16 @@ shiro-crypto-hash ${project.version} + + org.apache.shiro + shiro-crypto-support-hashes-argon2 + ${project.version} + + + org.apache.shiro + shiro-crypto-support-hashes-bcrypt + ${project.version} + org.apache.shiro shiro-crypto-cipher 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 f1e04158e1..77f2442c28 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 @@ -34,7 +34,6 @@ import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory; import org.apache.shiro.crypto.hash.format.HashFormat; import org.apache.shiro.crypto.hash.format.HashFormatFactory; -import org.apache.shiro.crypto.hash.format.HexFormat; import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat; import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.lang.codec.Hex; From 25be94f8ef6328c7b94b422b2cd7d2615ad13e98 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Sat, 9 Jan 2021 11:41:35 +0100 Subject: [PATCH 09/11] [SHIRO-290] implemented review comments - remove at least MD2, MD5 and Sha1 - Remove unused support-hashes module - changed group and artifact-ids for new modules - fixed compilation issue in Hasher (needs more work though) - add "since 2.0" comments --- core/pom.xml | 10 +-- .../credential/HashedCredentialsMatcher.java | 9 +-- .../credential/Md2CredentialsMatcher.java | 47 ------------- .../credential/Md5CredentialsMatcher.java | 46 ------------ .../credential/Sha1CredentialsMatcher.java | 46 ------------ .../DefaultPasswordServiceTest.groovy | 4 +- .../HashedCredentialsMatcherTest.java | 24 ++++--- .../credential/Md2CredentialsMatcherTest.java | 39 ----------- .../credential/Md5CredentialsMatcherTest.java | 37 ---------- .../Sha1CredentialsMatcherTest.java | 37 ---------- .../shiro/crypto/hash/AbstractCryptHash.java | 3 + .../crypto/hash/ConfigurableHashService.java | 8 +-- .../shiro/crypto/hash/DefaultHashService.java | 7 -- .../org/apache/shiro/crypto/hash/Hash.java | 3 - .../shiro/crypto/hash/HashProvider.java | 2 + .../org/apache/shiro/crypto/hash/HashSpi.java | 3 + .../org/apache/shiro/crypto/hash/Md2Hash.java | 68 ------------------ .../org/apache/shiro/crypto/hash/Md5Hash.java | 69 ------------------ .../apache/shiro/crypto/hash/Sha1Hash.java | 70 ------------------- .../shiro/crypto/hash/SimpleHashProvider.java | 6 +- .../crypto/hash/format/Shiro2CryptFormat.java | 2 +- .../hash/format/Base64FormatTest.groovy | 4 +- .../DefaultHashFormatFactoryTest.groovy | 6 +- .../crypto/hash/format/HexFormatTest.groovy | 4 +- crypto/support/hashes/argon2/pom.xml | 8 +-- .../support/hashes/argon2/Argon2Hash.java | 2 + .../hashes/argon2/Argon2HashProvider.java | 5 +- crypto/support/hashes/bcrypt/pom.xml | 8 +-- .../support/hashes/bcrypt/BCryptHash.java | 3 + .../support/hashes/bcrypt/BCryptProvider.java | 3 + .../support/hashes/bcrypt/OpenBSDBase64.java | 2 + .../hashes/bcrypt/BCryptHashTest.groovy | 3 + crypto/support/hashes/pom.xml | 41 ----------- crypto/support/pom.xml | 6 +- pom.xml | 8 +-- tools/hasher/pom.xml | 11 +++ .../org/apache/shiro/tools/hasher/Hasher.java | 11 +-- .../apache/shiro/tools/hasher/HasherTest.java | 3 + 38 files changed, 104 insertions(+), 564 deletions(-) delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java delete mode 100644 crypto/support/hashes/pom.xml diff --git a/core/pom.xml b/core/pom.xml index 179b35b3f1..5de40a9f81 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -102,12 +102,14 @@ shiro-crypto-hash - org.apache.shiro - shiro-crypto-support-hashes-argon2 + org.apache.shiro.crypto + shiro-hashes-argon2 + runtime - org.apache.shiro - shiro-crypto-support-hashes-bcrypt + org.apache.shiro.crypto + shiro-hashes-bcrypt + runtime org.apache.shiro 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 0b53eeed44..5e6b8ad73e 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 @@ -52,10 +52,7 @@ * and multiple hash iterations. Please read this excellent * Hashing Java article 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").

    *

    Real World Case Study

    * In April 2010, some public Atlassian Jira and Confluence * installations (Apache Software Foundation, Codehaus, etc) were the target of account attacks and user accounts @@ -115,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 */ 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 c968df5afe..0000000000 --- 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. - *

    - * Note: the MD2, MD5 and - * SHA-1 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 CredentialsMatcher implementations.

    - * - * @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 81b8f13c97..0000000000 --- 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. - *

    - * Note: MD5 and - * SHA-1 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 CredentialsMatcher implementations.

    - * - * @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/Sha1CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java deleted file mode 100644 index 6cdd328923..0000000000 --- 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. - *

    - * Note: MD5 and - * SHA-1 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 CredentialsMatcher implementations.

    - * - * @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/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy index 0e370bef96..ffd3ad8ca2 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 @@ -73,7 +73,7 @@ class DefaultPasswordServiceTest { assertTrue service.passwordsMatch(submitted, encrypted); //change some settings: - service.hashService.defaultAlgorithmName = "MD5" + service.hashService.defaultAlgorithmName = "SHA-512" def encrypted2 = service.encryptPassword(submitted) @@ -99,7 +99,7 @@ class DefaultPasswordServiceTest { assertTrue service.passwordsMatch("", (String) null) assertTrue service.passwordsMatch(null, "") assertFalse service.passwordsMatch(null, "12345") - assertFalse service.passwordsMatch(null, new Sha1Hash("test")) + assertFalse service.passwordsMatch(null, new Sha384Hash("test")) } @Test 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 6c9891f235..100a9c8895 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 @@ public class HashedCredentialsMatcherTest { @Test public void testSaltedAuthenticationInfo() { //use SHA-1 hashing in this test: - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //simulate a user account with a SHA-1 hashed and salted password: ByteSource salt = new SecureRandomNumberGenerator().nextBytes(); - Object hashedPassword = new Sha1Hash("password", salt); + Object hashedPassword = new Sha512Hash("password", salt); SimpleAuthenticationInfo account = new SimpleAuthenticationInfo("username", hashedPassword, salt, "realmName"); //simulate a username/password (plaintext) token created in response to a login attempt: @@ -63,17 +63,21 @@ public void testSaltedAuthenticationInfo() { */ @Test public void testBackwardsCompatibleUnsaltedAuthenticationInfo() { - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //simulate an account with SHA-1 hashed password (no salt) final String username = "username"; final String password = "password"; - final Object hashedPassword = new Sha1Hash(password).getBytes(); + final Object hashedPassword = new Sha512Hash(password).getBytes(); AuthenticationInfo account = new AuthenticationInfo() { + private static final long serialVersionUID = -3613684957517438801L; + + @Override public PrincipalCollection getPrincipals() { return new SimplePrincipalCollection(username, "realmName"); } + @Override public Object getCredentials() { return hashedPassword; } @@ -92,7 +96,7 @@ public Object getCredentials() { */ @Test public void testBackwardsCompatibleSaltedAuthenticationInfo() { - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //enable this for Shiro 1.0 backwards compatibility: matcher.setHashSalted(true); @@ -100,12 +104,16 @@ public void testBackwardsCompatibleSaltedAuthenticationInfo() { //(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 5286a58a2e..0000000000 --- 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 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 4c9d71dc32..0000000000 --- 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 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 29d6283658..0000000000 --- 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 getMatcherClass() { - return Sha1CredentialsMatcher.class; - } - - public AbstractHash hash(Object credentials) { - return new Sha1Hash(credentials); - } -} 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 index 29eb70f207..e873ad0653 100644 --- 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 @@ -32,6 +32,9 @@ import static java.util.Objects.requireNonNull; +/** + * @since 2.0.0 + */ public abstract class AbstractCryptHash implements Hash, Serializable { private static final long serialVersionUID = 2483214646921027859L; 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 0aa5701460..6e4dca5c4c 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 @@ -26,11 +26,11 @@ public interface ConfigurableHashService extends HashService { /** - * Sets the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute - * hashes. + * Sets the name of the key derivation function algorithm that will be used to compute + * secure hashes for passwords. * - * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to - * compute hashes. + * @param name the name of the key derivation function algorithm that will be used to + * compute secure hashes for passwords. */ void setDefaultAlgorithmName(String name); 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 24281d2b8a..ea3634764f 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 @@ -112,11 +112,4 @@ public String getDefaultAlgorithmName() { return this.defaultAlgorithmName; } - public Random getRandom() { - return random; - } - - public void setRandom(Random random) { - this.random = random; - } } 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 b0a9650fd2..7d74ebce05 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 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 index 84f90ce680..6a2528b511 100644 --- 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 @@ -29,6 +29,8 @@ * Hashes used by the Shiro2CryptFormat class. * *

    Instead of maintaining them as an {@code Enum}, ServiceLoaders would provide a pluggable alternative.

    + * + * @since 2.0.0 */ public final class HashProvider { 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 index f9bd8f6cb7..3b1b125019 100644 --- 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 @@ -22,6 +22,9 @@ import java.util.Random; import java.util.Set; +/** + * @since 2.0.0 + */ public interface HashSpi { Class getImplementationClass(); 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 a5e4d5b683..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java +++ /dev/null @@ -1,68 +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 source with an optional salt and - * hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - * @deprecated and will throw exceptions since 2.0.0, to be removed in 2.1.0. - */ -@Deprecated -public class Md2Hash extends SimpleHash { - - public static final String ALGORITHM_NAME = "MD2"; - private static final long serialVersionUID = 3432560796303546217L; - - 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 6cbb12699a..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java +++ /dev/null @@ -1,69 +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 source with an optional salt and - * hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - * @deprecated since 2.0.0, will throw exceptions in 2.1.0 and to be removed in 2.2.0. - */ -@Deprecated -public class Md5Hash extends SimpleHash { - - //TODO - complete JavaDoc - - public static final String ALGORITHM_NAME = "MD5"; - private static final long serialVersionUID = -1297357676456275955L; - - 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 e32860666b..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java +++ /dev/null @@ -1,70 +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 source with an - * optional salt and hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - * @deprecated since 2.0.0, will throw exceptions in 2.1.0 and to be removed in 2.2.0. - */ -@Deprecated -public class Sha1Hash extends SimpleHash { - - //TODO - complete JavaDoc - - public static final String ALGORITHM_NAME = "SHA-1"; - private static final long serialVersionUID = 3437720230699638519L; - - 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/SimpleHashProvider.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java index f916716f39..67b2fe777f 100644 --- 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 @@ -33,12 +33,12 @@ import static java.util.Collections.unmodifiableSet; import static java.util.stream.Collectors.toSet; +/** + * @since 2.0.0 + */ public class SimpleHashProvider implements HashSpi { private static final Set IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[]{ - Md2Hash.ALGORITHM_NAME, - Md5Hash.ALGORITHM_NAME, - Sha1Hash.ALGORITHM_NAME, Sha256Hash.ALGORITHM_NAME, Sha384Hash.ALGORITHM_NAME, Sha512Hash.ALGORITHM_NAME 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 index d0365e3254..11ce52d1ad 100644 --- 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 @@ -70,7 +70,7 @@ * * @see ModularCryptFormat * @see ParsableHashFormat - * @since 1.2 + * @since 2.0.0 */ public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat { 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 11eaa732ca..737eedcf30 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,7 +19,7 @@ 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.assertEquals @@ -34,7 +34,7 @@ class Base64FormatTest { @Test void testFormat() { - Hash hash = new Sha1Hash("hello"); + Hash hash = new Sha512Hash("hello"); Base64Format format = new Base64Format() String base64 = format.format(hash) assertEquals base64, hash.toBase64() 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 daa26e8371..10ddc09906 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,7 +18,7 @@ */ 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.* @@ -73,7 +73,7 @@ class DefaultHashFormatFactoryTest { @Test void testGetInstanceWithMcfFormattedString() { Shiro1CryptFormat format = new Shiro1CryptFormat() - def formatted = format.format(new Sha1Hash("test")) + def formatted = format.format(new Sha512Hash("test")) def factory = new DefaultHashFormatFactory() @@ -102,7 +102,7 @@ class DefaultHashFormatFactoryTest { void testMcfFormattedArgument() { def factory = new DefaultHashFormatFactory() - def hash = new Sha1Hash("test") + def hash = new Sha512Hash("test") def formatted = new Shiro1CryptFormat().format(hash) def instance = factory.getInstance(formatted) diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy index 30f18aa8e6..eaf0ac245c 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,7 +19,7 @@ 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.jupiter.api.Test import static org.junit.Assert.assertEquals @@ -34,7 +34,7 @@ class HexFormatTest { @Test void testFormat() { - Hash hash = new Sha1Hash("hello"); + Hash hash = new Sha512Hash("hello"); HexFormat format = new HexFormat() String hex = format.format(hash) assertEquals hex, hash.toHex() diff --git a/crypto/support/hashes/argon2/pom.xml b/crypto/support/hashes/argon2/pom.xml index 2a3a0f9a04..208a0542d0 100644 --- a/crypto/support/hashes/argon2/pom.xml +++ b/crypto/support/hashes/argon2/pom.xml @@ -24,13 +24,13 @@ 4.0.0 - org.apache.shiro - shiro-hashes + org.apache.shiro.crypto + shiro-crypto-support 2.0.0-SNAPSHOT - ../pom.xml + ../../pom.xml - shiro-crypto-support-hashes-argon2 + shiro-hashes-argon2 Apache Shiro :: Cryptography :: Support :: Hashes :: Argon2 bundle 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 index 9dd1f47dee..6f52f0c893 100644 --- 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 @@ -50,6 +50,8 @@ * The RFC suggests to use 1 GiB of memory for frontend and 4 GiB for backend authentication.

    * *

    Example crypt string is: {@code $argon2i$v=19$m=16384,t=100,p=2$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY}.

    + * + * @since 2.0.0 */ public class Argon2Hash extends AbstractCryptHash { private static final long serialVersionUID = 2647354947284558921L; 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 index 90a669f1c7..80e1d95c4a 100644 --- 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 @@ -33,6 +33,9 @@ import java.util.Random; import java.util.Set; +/** + * @since 2.0.0 + */ public class Argon2HashProvider implements HashSpi { private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class); @@ -140,7 +143,7 @@ private Optional intOrEmpty(Object maybeInt, String parameterName) { } } - static final class Parameters { + 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; diff --git a/crypto/support/hashes/bcrypt/pom.xml b/crypto/support/hashes/bcrypt/pom.xml index 9b9cc78306..24924d7833 100644 --- a/crypto/support/hashes/bcrypt/pom.xml +++ b/crypto/support/hashes/bcrypt/pom.xml @@ -24,13 +24,13 @@ 4.0.0 - org.apache.shiro - shiro-hashes + org.apache.shiro.crypto + shiro-crypto-support 2.0.0-SNAPSHOT - ../pom.xml + ../../pom.xml - shiro-crypto-support-hashes-bcrypt + shiro-hashes-bcrypt Apache Shiro :: Cryptography :: Support :: Hashes :: BCrypt bundle 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 index 028b3c38f2..0cceac9596 100644 --- 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 @@ -34,6 +34,9 @@ import static java.util.Collections.unmodifiableSet; +/** + * @since 2.0.0 + */ public class BCryptHash extends AbstractCryptHash { private static final long serialVersionUID = 6957869292324606101L; 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 index 0c76ff8a02..d4e7187dae 100644 --- 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 @@ -35,6 +35,9 @@ import java.util.Random; import java.util.Set; +/** + * @since 2.0.0 + */ public class BCryptProvider implements HashSpi { private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.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 index 531fa99f2c..804adac42a 100644 --- 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 @@ -31,6 +31,8 @@ * mostly-alphanumeric set of characters, plus . and /. Its 64-character set is "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz". * Padding is not used. * + * + * @since 2.0.0 */ public interface OpenBSDBase64 { 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 index 60415a0883..ee7eff4b2b 100644 --- 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 @@ -29,6 +29,9 @@ 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.0 + */ class BCryptHashTest { private static final String TEST_PASSWORD = "secret#shiro,password;Jo8opech"; diff --git a/crypto/support/hashes/pom.xml b/crypto/support/hashes/pom.xml deleted file mode 100644 index 6cea07c57a..0000000000 --- a/crypto/support/hashes/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - org.apache.shiro - shiro-support - 2.0.0-SNAPSHOT - ../../../support/pom.xml - - - 4.0.0 - shiro-hashes - Apache Shiro :: Cryptography :: Support :: Hashes - pom - - - bcrypt - argon2 - - - diff --git a/crypto/support/pom.xml b/crypto/support/pom.xml index 87aa859f80..582fe24b6d 100644 --- a/crypto/support/pom.xml +++ b/crypto/support/pom.xml @@ -30,12 +30,14 @@ ../pom.xml - support + org.apache.shiro.crypto + shiro-crypto-support Apache Shiro :: Cryptography :: Support pom - hashes + hashes/argon2 + hashes/bcrypt diff --git a/pom.xml b/pom.xml index 20739888a4..1ca995bef6 100644 --- a/pom.xml +++ b/pom.xml @@ -752,13 +752,13 @@ ${project.version} - org.apache.shiro - shiro-crypto-support-hashes-argon2 + org.apache.shiro.crypto + shiro-hashes-argon2 ${project.version} - org.apache.shiro - shiro-crypto-support-hashes-bcrypt + org.apache.shiro.crypto + shiro-hashes-bcrypt ${project.version} diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml index 77bc892210..2a3b4aa332 100644 --- a/tools/hasher/pom.xml +++ b/tools/hasher/pom.xml @@ -44,6 +44,17 @@ + + + org.apache.shiro.crypto + shiro-hashes-argon2 + compile + + + org.apache.shiro.crypto + shiro-hashes-bcrypt + compile + commons-cli commons-cli 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 77f2442c28..020d6d4298 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 @@ -34,7 +34,9 @@ import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory; import org.apache.shiro.crypto.hash.format.HashFormat; import org.apache.shiro.crypto.hash.format.HashFormatFactory; +import org.apache.shiro.crypto.hash.format.HexFormat; import org.apache.shiro.crypto.hash.format.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; @@ -49,6 +51,8 @@ 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). *

    @@ -69,7 +73,7 @@ public final class Hasher { 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 Argon2 when password hashing, SHA-512 otherwise."); private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information."); @@ -228,11 +232,10 @@ public static void main(String[] args) { } 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) { 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 index 79c5ceb117..bc7571a13d 100644 --- 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 @@ -35,6 +35,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * @since 2.0.0 + */ public class HasherTest { private final InputStream systemIn = System.in; From 2b39258f006770d7d7338c3da16d6208897e97f9 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Sat, 9 Jan 2021 14:03:04 +0100 Subject: [PATCH 10/11] [SHIRO-290] add some javadoc, make implementation classes package-private. --- .../shiro/crypto/hash/DefaultHashService.java | 6 +- .../shiro/crypto/hash/HashProvider.java | 8 +-- .../org/apache/shiro/crypto/hash/HashSpi.java | 38 +++++++++--- .../shiro/crypto/hash/SimpleHashProvider.java | 11 +--- .../crypto/hash/format/Shiro2CryptFormat.java | 2 +- .../support/hashes/argon2/Argon2Hash.java | 10 +++- .../hashes/argon2/Argon2HashProvider.java | 58 ++++++++++++++++--- .../support/hashes/bcrypt/BCryptHash.java | 8 ++- .../support/hashes/bcrypt/BCryptProvider.java | 17 ++---- .../support/hashes/bcrypt/OpenBSDBase64.java | 2 +- 10 files changed, 111 insertions(+), 49 deletions(-) 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 ea3634764f..bdb4e2373c 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 @@ -26,7 +26,7 @@ /** * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name. *

    Hash Algorithm

    - * 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, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}. @@ -88,9 +88,9 @@ public Hash computeHash(HashRequest request) { String algorithmName = getAlgorithmName(request); - Optional> kdfHash = HashProvider.getByAlgorithmName(algorithmName); + Optional kdfHash = HashProvider.getByAlgorithmName(algorithmName); if (kdfHash.isPresent()) { - HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new); + HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new); return hashSpi.newHashFactory(random).generate(request); } 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 index 6a2528b511..aeaca881ec 100644 --- 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 @@ -45,9 +45,9 @@ private HashProvider() { * @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> getByAlgorithmName(String algorithmName) { + public static Optional getByAlgorithmName(String algorithmName) { requireNonNull(algorithmName, "algorithmName in HashProvider.getByAlgorithmName"); - ServiceLoader> hashSpis = load(); + ServiceLoader hashSpis = load(); return StreamSupport.stream(hashSpis.spliterator(), false) .filter(hashSpi -> hashSpi.getImplementedAlgorithms().contains(algorithmName)) @@ -55,8 +55,8 @@ public static Optional> getByAlgorithmName(String algori } @SuppressWarnings("unchecked") - private static ServiceLoader> load() { - return (ServiceLoader>) (Object) ServiceLoader.load(HashSpi.class); + private static ServiceLoader load() { + return ServiceLoader.load(HashSpi.class); } } 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 index 3b1b125019..cf4cf87800 100644 --- 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 @@ -23,15 +23,39 @@ import java.util.Set; /** + * Service Provider Interface for password hashing algorithms. + * + *

    Apache Shiro will load algorithm implementations based on the method {@link #getImplementedAlgorithms()}. + * Loaded providers are expected to return a suitable hash implementation.

    + * + *

    Modern kdf-based hash implementations can extend the {@link AbstractCryptHash} class.

    + * * @since 2.0.0 */ -public interface HashSpi { - - Class getImplementationClass(); +public interface HashSpi { + /** + * A list of algorithms recognized by this implementation. + * + *

    Example values are {@code argon2id} and {@code argon2i} for the Argon2 service provider and + * {@code 2y} and {@code 2a} for the BCrypt service provider.

    + * + * @return a set of recognized algorithms. + */ Set getImplementedAlgorithms(); - T fromString(String format); + /** + * Creates a Hash instance from the given format string recognized by this provider. + * + *

    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.

    + * + *

    Implementations should overwrite this javadoc to add examples of the accepted formats.

    + * + * @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 }. @@ -42,10 +66,10 @@ public interface HashSpi { * @param random a source of {@link Random}, usually {@code SecureRandom}. * @return a factory class for creating instances of {@code }. */ - HashFactory newHashFactory(Random random); + HashFactory newHashFactory(Random random); - interface HashFactory { + interface HashFactory { - T generate(HashRequest hashRequest); + Hash generate(HashRequest hashRequest); } } 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 index 67b2fe777f..aa272fa9b5 100644 --- 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 @@ -36,7 +36,7 @@ /** * @since 2.0.0 */ -public class SimpleHashProvider implements HashSpi { +public class SimpleHashProvider implements HashSpi { private static final Set IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[]{ Sha256Hash.ALGORITHM_NAME, @@ -45,11 +45,6 @@ public class SimpleHashProvider implements HashSpi { }) .collect(toSet()); - @Override - public Class getImplementationClass() { - return SimpleHash.class; - } - @Override public Set getImplementedAlgorithms() { return unmodifiableSet(IMPLEMENTED_ALGORITHMS); @@ -67,11 +62,11 @@ public SimpleHash fromString(String format) { } @Override - public HashFactory newHashFactory(Random random) { + public HashFactory newHashFactory(Random random) { return new SimpleHashFactory(random); } - static class SimpleHashFactory implements HashSpi.HashFactory { + static class SimpleHashFactory implements HashSpi.HashFactory { private final Random random; 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 index 11ce52d1ad..31c7681af7 100644 --- 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 @@ -129,7 +129,7 @@ public Hash parse(final String formatted) { final String[] parts = suffix.split("\\$"); final String algorithmName = parts[0]; - HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName) + HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName) .orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented.")); return kdfHash.fromString("$" + suffix); } 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 index 6f52f0c893..c6a4a3a810 100644 --- 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 @@ -53,7 +53,7 @@ * * @since 2.0.0 */ -public class Argon2Hash extends AbstractCryptHash { +class Argon2Hash extends AbstractCryptHash { private static final long serialVersionUID = 2647354947284558921L; public static final String DEFAULT_ALGORITHM_NAME = "argon2id"; @@ -101,8 +101,12 @@ public static Set getAlgorithmsArgon2() { return unmodifiableSet(ALGORITHMS_ARGON2); } - public static ByteSource createSalt() { - return new SimpleByteSource(new SecureRandom().generateSeed(SALT_LENGTH)); + protected static ByteSource createSalt() { + return createSalt(new SecureRandom()); + } + + public static ByteSource createSalt(SecureRandom random) { + return new SimpleByteSource(random.generateSeed(SALT_LENGTH)); } public static Argon2Hash fromString(String input) { 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 index 80e1d95c4a..e56f5d3fba 100644 --- 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 @@ -34,17 +34,20 @@ import java.util.Set; /** + * A HashProvider for the Argon2 hash algorithm. + * + *

    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.

    + * + *

    Furthermore, there is a nested {@link Parameters} class which provides names for the + * keys used in the parameters map of the {@link HashRequest} class.

    + * * @since 2.0.0 */ -public class Argon2HashProvider implements HashSpi { +public class Argon2HashProvider implements HashSpi { private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class); - @Override - public Class getImplementationClass() { - return Argon2Hash.class; - } - @Override public Set getImplementedAlgorithms() { return Argon2Hash.getAlgorithmsArgon2(); @@ -56,11 +59,11 @@ public Argon2Hash fromString(String format) { } @Override - public HashFactory newHashFactory(Random random) { + public HashFactory newHashFactory(Random random) { return new Argon2HashFactory(random); } - static class Argon2HashFactory implements HashSpi.HashFactory { + static class Argon2HashFactory implements HashSpi.HashFactory { private final SecureRandom random; @@ -117,7 +120,7 @@ private ByteSource parseSalt(HashRequest hashRequest) { .map(saltParm -> Base64.getDecoder().decode((String) saltParm)) .map(SimpleByteSource::new) .flatMap(this::lengthValidOrEmpty) - .orElseGet(Argon2Hash::createSalt); + .orElseGet(() -> Argon2Hash.createSalt(random)); } private Optional lengthValidOrEmpty(ByteSource bytes) { @@ -143,6 +146,15 @@ private Optional intOrEmpty(Object maybeInt, String parameterName) { } } + /** + * Parameters for the {@link Argon2Hash} class. + * + *

    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.

    + * + *

    The constants starting with {@code DEFAULT_} are their respective default values.

    + */ public static final class Parameters { public static final String DEFAULT_ALGORITHM_NAME = Argon2Hash.DEFAULT_ALGORITHM_NAME; @@ -152,12 +164,40 @@ public static final class Parameters { public static final int DEFAULT_PARALLELISM = Argon2Hash.DEFAULT_PARALLELISM; public static final int DEFAULT_OUTPUT_LENGTH = Argon2Hash.DEFAULT_OUTPUT_LENGTH; + /** + * Parameter for modifying the internal algorithm used by Argon2. + * + *

    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).

    + * + *

    The default value is {@value DEFAULT_ALGORITHM_NAME} when this parameter is not specified.

    + */ public static final String PARAMETER_ALGORITHM_NAME = "Argon2.algorithmName"; public static final String PARAMETER_ALGORITHM_VERSION = "Argon2.version"; + + /** + * The salt to use. + * + *

    The value for this parameter accepts a Base64-encoded 16byte (128bit) salt.

    + * + *

    As for any KDF, do not use a static salt value for multiple passwords.

    + * + *

    The default value is a new random 128bit-salt, if this parameter is not specified.

    + */ public static final String PARAMETER_SALT = "Argon2.salt"; + public static final String PARAMETER_ITERATIONS = "Argon2.iterations"; public static final String PARAMETER_MEMORY_KIB = "Argon2.memoryKib"; public static final String PARAMETER_PARALLELISM = "Argon2.parallelism"; + + /** + * The output length of the resulting data section. + * + *

    Argon2 allows to modify the length of the generated output.

    + * + *

    The default value is {@value DEFAULT_OUTPUT_LENGTH} when this parameter is not specified.

    + */ public static final String PARAMETER_OUTPUT_LENGTH = "Argon2.outputLength"; private Parameters() { 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 index 0cceac9596..b2b10c008c 100644 --- 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 @@ -37,7 +37,7 @@ /** * @since 2.0.0 */ -public class BCryptHash extends AbstractCryptHash { +class BCryptHash extends AbstractCryptHash { private static final long serialVersionUID = 6957869292324606101L; @@ -142,7 +142,11 @@ public static BCryptHash generate(String algorithmName, ByteSource source, ByteS } protected static ByteSource createSalt() { - return new SimpleByteSource(new SecureRandom().generateSeed(SALT_LENGTH)); + return createSalt(new SecureRandom()); + } + + protected static ByteSource createSalt(SecureRandom random) { + return new SimpleByteSource(random.generateSeed(SALT_LENGTH)); } @Override 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 index d4e7187dae..4e6d093347 100644 --- 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 @@ -38,15 +38,10 @@ /** * @since 2.0.0 */ -public class BCryptProvider implements HashSpi { +public class BCryptProvider implements HashSpi { private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.class); - @Override - public Class getImplementationClass() { - return BCryptHash.class; - } - @Override public Set getImplementedAlgorithms() { return BCryptHash.getAlgorithmsBcrypt(); @@ -58,11 +53,11 @@ public BCryptHash fromString(String format) { } @Override - public HashFactory newHashFactory(Random random) { + public HashFactory newHashFactory(Random random) { return new BCryptHashFactory(random); } - static class BCryptHashFactory implements HashSpi.HashFactory { + static class BCryptHashFactory implements HashSpi.HashFactory { private final SecureRandom random; @@ -122,21 +117,21 @@ private ByteSource getSalt(HashRequest hashRequest) { .map(obj -> (String) obj); if (!optSaltBase64.isPresent()) { - return BCryptHash.createSalt(); + 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(); + return BCryptHash.createSalt(random); } return new SimpleByteSource(saltBytes); } } - static final class Parameters { + public static final class Parameters { public static final String DEFAULT_ALGORITHM_NAME = BCryptHash.DEFAULT_ALGORITHM_NAME; public static final String PARAMETER_SALT = "BCrypt.salt"; 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 index 804adac42a..1dc080b699 100644 --- 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 @@ -34,7 +34,7 @@ * * @since 2.0.0 */ -public interface OpenBSDBase64 { +interface OpenBSDBase64 { /** From ff57cf447308a6455261aeb6e333b28da6900f7a Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Wed, 13 Jan 2021 15:51:56 +0100 Subject: [PATCH 11/11] [SHIRO-290] doc updates --- RELEASE-NOTES | 20 +++- .../credential/DefaultPasswordService.java | 8 ++ .../authc/credential/PasswordMatcher.java | 11 --- .../shiro/crypto/hash/AbstractCryptHash.java | 91 ++++++++++++++----- .../shiro/crypto/hash/DefaultHashService.java | 2 +- .../org/apache/shiro/crypto/hash/Hash.java | 8 ++ .../shiro/crypto/hash/HashProvider.java | 2 +- .../org/apache/shiro/crypto/hash/HashSpi.java | 14 ++- .../apache/shiro/crypto/hash/SimpleHash.java | 13 ++- .../shiro/crypto/hash/SimpleHashProvider.java | 66 +++++++------- .../crypto/hash/format/Shiro2CryptFormat.java | 8 +- .../support/hashes/argon2/Argon2Hash.java | 19 +++- .../hashes/argon2/Argon2HashProvider.java | 2 +- .../support/hashes/bcrypt/BCryptHash.java | 22 +++-- .../support/hashes/bcrypt/BCryptProvider.java | 2 +- .../support/hashes/bcrypt/OpenBSDBase64.java | 19 +--- .../hashes/bcrypt/BCryptHashTest.groovy | 2 +- .../apache/shiro/tools/hasher/HasherTest.java | 2 +- 18 files changed, 206 insertions(+), 105 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 7b39af3b5d..42bf34243f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -17,9 +17,27 @@ This is not an official release notes document. It exists for Shiro developers to jot down their notes while working in the source code. These notes will be -combined with Jira's auto-generated release notes during a release for the +combined with Jira’s auto-generated release notes during a release for the total set. +########################################################### +# 2.0.0 +########################################################### + +Improvement + + [SHIRO-290] Implement bcrypt and argon2 KDF algorithms + +Backwards Incompatible Changes +-------------------------------- + +* Changed default DefaultPasswordService.java algorithm to "Argon2id". +* PasswordService.encryptPassword(Object plaintext) will now throw a NullPointerException on null parameter. + It was never specified how this method would behave. +* Made salt non-nullable. +* Removed methods in PasswordMatcher. + + ########################################################### # 1.5.3 ########################################################### diff --git a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java index 81d846bebe..6c0578f16e 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 @@ -57,6 +57,14 @@ public class DefaultPasswordService implements HashingPasswordService { private volatile boolean hashFormatWarned; //used to avoid excessive log noise + /** + * Constructs a new PasswordService with a default hash service and the default + * algorithm name {@value #DEFAULT_HASH_ALGORITHM}, a default hash format (shiro2) and + * a default hashformat factory. + * + *

    The default algorithm can change between minor versions and does not introduce + * API incompatibility by design.

    + */ public DefaultPasswordService() { this.hashFormatWarned = false; 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 d5d489fb5e..dd60a850b4 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 @@ -58,17 +58,6 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo return service.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); - } - private PasswordService ensurePasswordService() { PasswordService service = getPasswordService(); if (service == null) { 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 index e873ad0653..f056c617ff 100644 --- 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 @@ -24,16 +24,26 @@ import org.apache.shiro.lang.util.ByteSource; import java.io.Serializable; -import java.security.MessageDigest; +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; /** - * @since 2.0.0 + * Abstract class for hashes following the posix crypt(3) format. + * + *

    These implementations must contain a salt, a salt length, can format themselves to a valid String + * suitable for the {@code /etc/shadow} file.

    + * + *

    It also defines the hex and base64 output by wrapping the output of {@link #formatToCryptString()}.

    + * + *

    Implementation notice: Implementations should provide a static {@code fromString()} method.

    + * + * @since 2.0 */ public abstract class AbstractCryptHash implements Hash, Serializable { @@ -54,6 +64,16 @@ public abstract class AbstractCryptHash implements Hash, Serializable { */ private String base64Encoded; + /** + * Constructs an {@link AbstractCryptHash} using the algorithm name, hashed data and salt parameters. + * + *

    Other required parameters must be stored by the implementation.

    + * + * @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); @@ -67,9 +87,26 @@ protected final void checkValid() { checkValidSalt(); } + /** + * Algorithm-specific checks of the algorithm’s parameters. + * + *

    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.

    + * + * @throws IllegalArgumentException if any of the parameters are invalid. + */ protected abstract void checkValidAlgorithm(); - private void checkValidSalt() { + /** + * 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( @@ -110,6 +147,13 @@ 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); @@ -121,32 +165,32 @@ public boolean isEmpty() { } /** - * Returns a hex-encoded string of the underlying {@link #getBytes byte array}. + * Returns a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}. *

    * This implementation caches the resulting hex string so multiple calls to this method remain efficient. * - * @return a hex-encoded string of the underlying {@link #getBytes byte array}. + * @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.getBytes()); + this.hexEncoded = Hex.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8)); } return this.hexEncoded; } /** - * Returns a Base64-encoded string of the underlying {@link #getBytes byte array}. + * Returns a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}. *

    * This implementation caches the resulting Base64 string so multiple calls to this method remain efficient. * - * @return a Base64-encoded string of the underlying {@link #getBytes byte array}. + * @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.getBytes()); + this.base64Encoded = Base64.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8)); } return this.base64Encoded; } @@ -160,33 +204,34 @@ public String toBase64() { public abstract String formatToCryptString(); /** - * Returns {@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. + * 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 (Hash) to check for equality. - * @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. + * @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 Hash) { - final Hash that = (Hash) other; - return MessageDigest.isEqual(this.getBytes(), that.getBytes()); + if (other instanceof AbstractCryptHash) { + final AbstractCryptHash that = (AbstractCryptHash) other; + return this.formatToCryptString().equals(that.formatToCryptString()); } return false; } /** - * Simply returns toHex().hashCode(); + * Hashes the formatted crypt string. * - * @return toHex().hashCode() + *

    Implementations should not override this method, as different algorithms produce different output formats + * and require different parameters.

    + * @return a hashcode from the {@link #formatToCryptString() formatted output}. */ @Override public int hashCode() { - if (this.getBytes() == null || this.getBytes().length == 0) { - return 0; - } - return Arrays.hashCode(this.getBytes()); + return Objects.hash(this.formatToCryptString()); } /** 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 bdb4e2373c..ed2653f1c6 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 @@ -43,7 +43,7 @@ */ public class DefaultHashService implements ConfigurableHashService { - private Random random; + private final Random random; /** * The MessageDigest name of the hash algorithm to use for computing hashes. 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 7d74ebce05..ce52ce8fd3 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 @@ -61,5 +61,13 @@ public interface Hash extends ByteSource { */ int getIterations(); + /** + * Tests if a given passwords matches with this instance. + * + *

    Usually implementations will re-create {@code this} but with the given plaintext bytes as secret.

    + * + * @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 index aeaca881ec..64de6f935b 100644 --- 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 @@ -30,7 +30,7 @@ * *

    Instead of maintaining them as an {@code Enum}, ServiceLoaders would provide a pluggable alternative.

    * - * @since 2.0.0 + * @since 2.0 */ public final class HashProvider { 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 index cf4cf87800..de4f2cf65a 100644 --- 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 @@ -30,7 +30,7 @@ * *

    Modern kdf-based hash implementations can extend the {@link AbstractCryptHash} class.

    * - * @since 2.0.0 + * @since 2.0 */ public interface HashSpi { @@ -70,6 +70,18 @@ public interface HashSpi { interface HashFactory { + /** + * Generates a hash from the given hash request. + * + *

    If the hash requests’ optional parameters are not set, the {@link HashFactory} implementation + * should use default parameters where applicable.

    + *

    If the hash requests’ salt is missing or empty, the implementation should create a salt + * with a default size.

    + * @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/SimpleHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java index c8b8e7ef89..eb58a895ef 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 @@ -25,6 +25,8 @@ 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; @@ -48,6 +50,8 @@ 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. */ @@ -272,7 +276,14 @@ public int getIterations() { @Override public boolean matchesPassword(ByteSource plaintextBytes) { - return this.equals(new SimpleHash(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations())); + 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 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 index aa272fa9b5..5b4a44df2c 100644 --- 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 @@ -34,7 +34,9 @@ import static java.util.stream.Collectors.toSet; /** - * @since 2.0.0 + * 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 { @@ -87,6 +89,36 @@ public SimpleHash generate(HashRequest hashRequest) { return createSimpleHash(algorithmName, source, iterations, publicSalt, salt); } + /** + * Returns the public salt that should be used to compute a hash based on the specified request. + *

    + * This implementation functions as follows: + *

      + *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. + *
    3. If the request salt is null or empty: + *
      1. create a new 16-byte salt.
      + *
    4. + *
    + * + * @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 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 secretSalt = Optional.ofNullable(request.getParameters().get(Parameters.PARAMETER_SECRET_SALT)); @@ -125,38 +157,6 @@ protected int getIterations(HashRequest request) { return iterations; } - /** - * 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. - *

    - * This implementation functions as follows: - *

      - *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. - *
    3. If the request salt is null or empty: - *
        - *
      - *
    4. - *
    - * - * @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 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); - } - /** * 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 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 index 31c7681af7..7cc3ff1e38 100644 --- 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 @@ -70,11 +70,17 @@ * * @see ModularCryptFormat * @see ParsableHashFormat - * @since 2.0.0 + * @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() { 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 index c6a4a3a810..fee355206c 100644 --- 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 @@ -25,6 +25,8 @@ 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; @@ -51,11 +53,13 @@ * *

    Example crypt string is: {@code $argon2i$v=19$m=16384,t=100,p=2$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY}.

    * - * @since 2.0.0 + * @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; @@ -204,7 +208,7 @@ public static Argon2Hash generate( type = Argon2Parameters.ARGON2_id; break; default: - throw new UnsupportedOperationException("Unknown argon2 algorithm: " + algorithmName); + throw new IllegalArgumentException("Unknown argon2 algorithm: " + algorithmName); } final Argon2Parameters parameters = new Argon2Parameters.Builder(type) @@ -257,8 +261,15 @@ public int getIterations() { @Override public boolean matchesPassword(ByteSource plaintextBytes) { - Argon2Hash compare = generate(this.getAlgorithmName(), this.argonVersion, plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism, this.getBytes().length); - return this.equals(compare); + try { + Argon2Hash compare = generate(this.getAlgorithmName(), this.argonVersion, plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism, this.getBytes().length); + + return this.equals(compare); + } catch (IllegalArgumentException illegalArgumentException) { + // cannot recreate hash. Do not log password. + LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException); + return false; + } } @Override 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 index e56f5d3fba..fdacbc3729 100644 --- 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 @@ -42,7 +42,7 @@ *

    Furthermore, there is a nested {@link Parameters} class which provides names for the * keys used in the parameters map of the {@link HashRequest} class.

    * - * @since 2.0.0 + * @since 2.0 */ public class Argon2HashProvider implements HashSpi { 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 index b2b10c008c..f73b40a560 100644 --- 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 @@ -23,6 +23,8 @@ 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; @@ -35,12 +37,14 @@ import static java.util.Collections.unmodifiableSet; /** - * @since 2.0.0 + * @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; @@ -103,7 +107,7 @@ public static BCryptHash fromString(String input) { // the input string should look like this: // $2y$cost$salt{22}hash if (!input.startsWith("$")) { - throw new UnsupportedOperationException("Unsupported input: " + input); + throw new IllegalArgumentException("Unsupported input: " + input); } final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1)); @@ -174,10 +178,16 @@ public int getIterations() { @Override public boolean matchesPassword(ByteSource plaintextBytes) { - final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); - BCryptHash other = fromString(cryptString); - - return this.equals(other); + 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 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 index 4e6d093347..74961564b3 100644 --- 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 @@ -36,7 +36,7 @@ import java.util.Set; /** - * @since 2.0.0 + * @since 2.0 */ public class BCryptProvider implements HashSpi { 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 index 1dc080b699..ad05fe853b 100644 --- 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 @@ -32,7 +32,7 @@ * Padding is not used. * * - * @since 2.0.0 + * @since 2.0 */ interface OpenBSDBase64 { @@ -53,23 +53,6 @@ interface OpenBSDBase64 { */ byte[] decode(byte[] utf8EncodedRadix64String); - /* - * 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. - */ - /** * A mod of Square's Okio Base64 encoder *

    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 index ee7eff4b2b..f95e1a22f8 100644 --- 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 @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertTrue /** - * @since 2.0.0 + * @since 2.0 */ class BCryptHashTest { 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 index bc7571a13d..00e6286027 100644 --- 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 @@ -36,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * @since 2.0.0 + * @since 2.0 */ public class HasherTest {