Apache Shiro HashedCredentialsMatcher 生成错误的盐

Apache Shiro HashedCredentialsMatcher generates wrong salt

我遇到的问题是 Shiro 在转换字节时表现出一些奇怪的行为 数组到盐。

我开始将流程中涉及的所有 类 实施到我的应用程序中,它们是:

创建用户后,用户密码会使用生成的盐进行哈希处理,然后存储在我的数据库中:

import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;

RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();
String hashedPasswordBase64 = new Sha256Hash(password, salt, 1024).toBase64();

shiro.ini 看起来像这样:

# SALTED JDBC REALM

saltedJdbcRealm=com.mycompany.ssp.SaltedJdbcRealm

dataSource = org.postgresql.ds.PGSimpleDataSource
dataSource.databaseName = Self-Service-Portal
dataSource.serverName = localhost
dataSource.portNumber = 5432
dataSource.user = postgres
dataSource.password = admin

saltedJdbcRealm.dataSource = $dataSource
saltedJdbcRealm.authenticationQuery = SELECT umgmt_users.password, umgmt_users.salt FROM umgmt_users WHERE umgmt_users.user = ?

sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher

# base64 encoding, not hex in this example:
sha256Matcher.storedCredentialsHexEncoded = false
sha256Matcher.hashIterations = 1024

saltedJdbcRealm.credentialsMatcher = $sha256Matcher

################################################################################
# SECURITY MANAGER #

securityManager.realms = $saltedJdbcRealm
strategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $strategy

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

我的自定义 saltedJdbcRealm 只是覆盖了 doGetAuthenticationInfo。此代码来自此博客->

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //identify account to log to
    UsernamePasswordToken userPassToken = (UsernamePasswordToken) token;
    String username = userPassToken.getUsername();

    if (username == null) {
        log.debug("Username is null.");
        return null;
    }

    // read password hash and salt from db 
    PasswdSalt passwdSalt = getPasswordForUser(username);

    if (passwdSalt == null) {
        log.debug("No account found for user [" + username + "]");
        return null;
    }

    // return salted credentials
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, passwdSalt.password, getName());
    info.setCredentialsSalt(new SimpleByteSource(passwdSalt.salt));

    return info;
}

return info 之后的调试是这样的:

寻找错误我最终在这里找到了它 org.apache.shiro.authc.credential.HashedCredentialsMatcher.java:

protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
    Object salt = null;
    if (info instanceof SaltedAuthenticationInfo) {

        // STOP HERE AND SEE BELOW PART 1!!!

        salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();

        // STOP HERE AND SEE BELOW PART 2!!!

    } else {
        //retain 1.0 backwards compatibility:
        if (isHashSalted()) {
            salt = getSalt(token);
        }
    }
    return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}

第 1 部分:

让我们看一下变量信息:

完整的字节数组如下:

57 109 102 43 65 87 118 88 70 76 105 82 116 104 113 108 116 100 101 108 79 119 61 61

它在我的数据库中正确地代表了盐:

9mf+AWvXFLiRthqltdelOw==

代码的下一步是从 info 变量中提取 Salt,并将其存储在 Object 类型的变量 salt 中。

第 2 部分:

查看此行之后的变量盐:

salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();

执行我得到这个结果:

OW1mK0FXdlhGTGlSdGhxbHRkZWxPdz09

编辑:

我做了另一个例子并向您展示了 2 种方法 1) 对提交的密码进行哈希处理 2) 从数据库中获取密码进行比较,但它们不是 相同:

我从 2 个变量开始,token(提交的密码)和 info(存储的密码信息):

存储的凭据:

凭据:

d5fHxI7kYQYtyqo6kwvZFDATIIsZThvFQeDVidpDDEQ

解码前的存储字节数:

100 53 102 72 120 73 55 107 89 81 89 116 121 113 111 54 107 119 118 90 70 68 65 84 73 73 115 90 84 104 118 70 81 101 68 86 105 100 112 68 68 69 81 61

解码后的存储字节数:

119 -105 -57 -60 -114 -28 97 6 45 -54 -86 58 -109 11 -39 20 48 19 32 -117 25 78 27 -59 65 -32 -43 -119 -38 67 12 68

哈希:

7797c7c48ee461062dcaaa3a930bd9143013208b194e1bc541e0d589da430c44

提交的凭据:

char[] 凭据:

[0] = 1
[1] = 2
[2] = 3

byte[] 字节:

50 69 81 77 57 55 80 53 53 112 89 52 122 69 78 54 57 98 53 56 82 65 61 61

这是 2EQM97P55pY4zEN69b58RA== 这是数据库中的内容

cachedBase64:

MkVRTTk3UDU1cFk0ekVONjliNThSQT09

return 值是这个散列:

af9a7ef0ea9fa4d93eae1ca5d16c03c516f4822ec3e9017f14f694175848a6ab

由于 2 个哈希值不相同,我明白为什么我的应用程序告诉我错误的密码但是我使用上面的代码(第一个代码块)创建了这个密码为 123 的用户

编辑结束

所以有人知道为什么哈希计算没有为相同的密码给出相同的哈希值吗???或者我可能做错了什么(我怀疑 shiro 代码是错误的,所以我的代码生成密码 hash/salt 或 shiro.ini 配置可能有问题?)

ufff,在对这些函数进行更多尝试后,我找到了为什么提交的密码使用错误的 salt 值进行哈希处理的解决方案

我在方法hashProvidedCredentials里面加了3行

org.apache.shiro.authc.credential.HashedCredentialsMatcher.java

protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
    Object salt = null;
    if (info instanceof SaltedAuthenticationInfo) {
        salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
        
        // Get base64 Decoder
        java.util.Base64.Decoder Decoder = java.util.Base64.getDecoder();
        // decode salt from database
        byte[] encodedJava8 = null;
        encodedJava8 = Decoder.decode(((SaltedAuthenticationInfo) info).getCredentialsSalt().getBytes());
        // save decoded salt value in previous salt Object
        salt = ByteSource.Util.bytes(encodedJava8);
        
        // The 3 steps above are nessecary because the Object salt is of type 
        // SimpleByteSource and:
        // - it holds a byte[] which holds the salt in its correct form
        // - it also holds a cachedBase64 encoded version of this byte[]
        //   (which is of course not the actual salt)

        // The Problem is that the next method call below that hashes the
        // submitted password uses the cachedBase64 value to hash the
        // passwort and not the byte[] which represents the actual salt

        // Therefor it is nessecary to:
        // - create SimpleByteSource salt with the value from the database
        // - decode the byte[] so that the cachedBase64 represents the actual salt
        // - store the decoded version of the byte[] in the SimpleByteSource variable salt
    } else {
        //retain 1.0 backwards compatibility:
        if (isHashSalted()) {
            salt = getSalt(token);
        }
    }
    return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}

现在,用户在登录时提交的密码的散列方式与以这种方式生成时的方式相同:

RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();
//Now hash the plain-text password with the random salt and multiple
//iterations and then Base64-encode the value (requires less space than Hex):
String hashedPasswordBase64 = new Sha256Hash(password, salt, 1024).toBase64();

注意:这不是密码散列的最终版本。 Salt 将至少为 256 位,迭代次数将在 200k-300k 左右。

问题解决后,我将问题缩小为 4 个可能的选项:

1)

shiro 代码中存在一个重大错误 (HashedCredentialsMatcher.java)(至少从我的角度来看是这样)因为使用 salt 的密码验证总是会以这种方式失败(请参阅我在代码中的描述块)。

2)

我要么使用了错误的 CredentialsMatcher 作为 hased 和 salted 密码,我不知道应该使用哪一个。

3)

我在我的自定义领域中实现 doGetAuthenticationInfo 方法有一个错误。对于我的自定义领域,我使用了本教程: Apache Shiro Part 2 - Realms, Database and PGP Certificates

4)

我在创建密码哈希时犯了一个错误(尽管该代码来自 Apache Shiro 官方网站Link

从我的角度来看,选项 1 和 4 不是问题,所以它的 2 或 3 导致了这个问题,并且必须向 HashedCredentialsMatcher.java 方法添加一些代码:hashProvidedCredentials()

所以总结一下,有没有人对这个问题有任何想法只是为了得到澄清??