web前端

Hash加密算法

2018-05-07  本文已影响371人  spilledyear

关键词:Hash加密算法、Security中的PasswordManager
Hash与加密

密码安全

用户的密码,怎么才能够保证安全?我认为最完美的方法就是确保该密码只有用户自己知道。
在系统中,用户信息一般都是存储在数据库中,其中就包括账号、密码等信息。对于密码的存储方式,一般有两种

有些系统安全意识不够,直接存储明文,这是绝对不可取的,就算不考虑因为系统异常等因素导致的密码泄露,拥有数据库最高权限的人,就一定能看到所有用户的密码,这显然是不可取的。所以,主要考虑的是Hash存储。

转换算法目前主流的就是哈希算法,也叫译摘要算法,是一种散列算法。哈希算法是不可逆的,这里的不可逆有两层含义,一是“给定一个哈希结果R,没有方法将R转换成原目标文本S”,二是“给定哈希结果R,即使知道一段文本S的哈希结果为R,也不能断言当初的目标文本就是S”(这里涉及到Hash碰撞)。

为什么需要单向的算法?回到之前的那句话:“最完美的方法就是确保该密码只有用户自己知道”。单向的,就意味着对于每一个固定的明文,经过Hash算法转换后可以得到固定的Hash值,但是根据Hash值,却无法得到明文。数据库最高管理员可以看到不同用户密码对应的Hash值,但因为Hash算法是不可逆的,所以,他也无法知道用户的文明,没有明文,就无法登录系统进行危险操作。

但是仔细想想,上面的方案还是会有一些问题。我们知道,根据固定的明文,按照一定的Hash算法可以得到固定的Hash值。用户设置密码的时候,又基本上不会设置泰国负载的密码,那就有可能通过穷举法来实现破解,将常见密码的的Hash值全部算出来,然后拿用户的Hash值一一匹对,虽然,根据不同的明文生成的 hash 值可能相同(Hash碰撞),但这只会让破解更简单。

盐值
为了解决上面的问题,hash 方案迎来的第一个改造是对引入一个“随机的因子”来掺杂进明文中进行 hash 计算,这样的随机因子通常被称之为盐 (salt)。salt 一般是用户相关的,每个用户持有各自的 salt。此时两个用户的密码即使相同,由于 salt 的影响,存储在数据库中的密码也是不同的。

但现在的计算机能力越来越强,虽然破解 salted hash 比较麻烦,却并非不可行。一些新型的单向 hash 算法被研究了出来。其中就包括:Bcrypt,PBKDF2,Scrypt,Argon2。

Hash算法

MD5和SHA。SHA又包括 SHA-1 和 SHA-2(SHA-224、SHA-256、SHA-384、SHA-512) 和SHA-3。

PasswordManager

在spring security新版本中,获取明文的Hash值通过org.springframework.security.crypto.password.PasswordEncoder接口,该接口有三个实现:

image.png
NoOpPasswordEncoder不多说了,啥也不做按原文本处理,相当于不加密。

StandardPasswordEncoder 1024次迭代的SHA-256散列哈希加密实现,并使用一个随机8字节的salt。

BCryptPasswordEncoder 使用BCrypt的强散列哈希加密实现,并可以由客户端指定加密的强度strength,强度越高安全性自然就越高,默认为10.

Hap中使用的是StandardPasswordEncoder,但官方其实推荐使用BCryptPasswordEncoder。

 * If you are developing a new system,
 * {@link org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder} is a better
 * choice both in terms of security and interoperability with other languages.

Hap中的自定义PasswordManager如下,其实就是调用StandardPasswordEncoder:

<bean id="passwordManager" class="com.hand.hap.security.PasswordManager">
    <property name="siteWideSecret" value="Zxa1pO6S6uvBMlY"/>
</bean>
package com.hand.hap.security;

import java.util.Arrays;
import java.util.List;

import com.hand.hap.mybatis.util.StringUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;

import com.hand.hap.message.profile.SystemConfigListener;

/**
 * @author njq.niu@hand-china.com
 * @author xiangyu.qi@hand-china.com
 * @date 2016/1/31
 * @date 2016/10/10
 */
public class PasswordManager implements PasswordEncoder, InitializingBean, SystemConfigListener {

    public static final String PASSWORD_COMPLEXITY_NO_LIMIT = "NO_LIMIT";
    public static final String PASSWORD_COMPLEXITY_DIGITS_AND_LETTERS = "DIGITS_AND_LETTERS";
    public static final String PASSWORD_COMPLEXITY_DIGITS_AND_CASE_LETTERS = "DIGITS_AND_CASE_LETTERS";

    private PasswordEncoder delegate;

    private String siteWideSecret = "my-secret-key";

    private String defaultPassword = "123456";

    /**
     * 密码失效时间 默认0 不失效
     */
    private Integer passwordInvalidTime = 0;

    /**
     * 密码长度
     */
    private Integer passwordMinLength = 8;

    /**
     * 密码复杂度
     */
    private String passwordComplexity = "no_limit";

    public Integer getPasswordInvalidTime() {
        return passwordInvalidTime;
    }

    public Integer getPasswordMinLength() {
        return passwordMinLength;
    }


    public String getPasswordComplexity() {
        return passwordComplexity;
    }


    public String getDefaultPassword() {
        return defaultPassword;
    }


    public String getSiteWideSecret() {
        return siteWideSecret;
    }

    public void setSiteWideSecret(String siteWideSecret) {
        this.siteWideSecret = siteWideSecret;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        delegate = new StandardPasswordEncoder(siteWideSecret);
    }

    @Override
    public String encode(CharSequence rawPassword) {
        return delegate.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (StringUtil.isEmpty(encodedPassword)) {
            return false;
        }
        return delegate.matches(rawPassword, encodedPassword);
    }

    @Override
    public List<String> getAcceptedProfiles() {
        return Arrays.asList("DEFAULT_PASSWORD", "PASSWORD_INVALID_TIME", "PASSWORD_MIN_LENGTH", "PASSWORD_COMPLEXITY");
    }

    @Override
    public void updateProfile(String profileName, String profileValue) {
        if ("PASSWORD_INVALID_TIME".equalsIgnoreCase(profileName)) {
            this.passwordInvalidTime = Integer.parseInt(profileValue);
        } else if ("PASSWORD_MIN_LENGTH".equalsIgnoreCase(profileName)) {
            this.passwordMinLength = Integer.parseInt(profileValue);
        } else if ("PASSWORD_COMPLEXITY".equalsIgnoreCase(profileName)) {
            this.passwordComplexity = profileValue;
        } else if ("DEFAULT_PASSWORD".equalsIgnoreCase(profileName)) {
            this.defaultPassword = profileValue;
        }
    }
}  

主要有两个方法

    /**
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
     * greater hash combined with an 8-byte or greater randomly generated salt.
     */
    String encode(CharSequence rawPassword);

    /**
     * Verify the encoded password obtained from storage matches the submitted raw
     * password after it too is encoded. Returns true if the passwords match, false if
     * they do not. The stored password itself is never decoded.
     *
     * @param rawPassword the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return true if the raw password, after encoding, matches the encoded password from
     * storage
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);

encode 方法是將明文生成Hash值得,参数就是明文。matches使用来匹对密码是否正确的,第一个参数是明文,第二个参数是之前生成得Hash值。

encode过程

StandardPasswordEncoder两个共有构造函数

image.png

secret是秘钥,可以没有。在Hap中,配置文件中给了一个秘钥:Zxa1pO6S6uvBMlY

    <bean id="passwordManager" class="com.hand.hap.security.PasswordManager">
        <property name="siteWideSecret" value="Zxa1pO6S6uvBMlY"/>
    </bean>

调用encode方法的时候,基于SHA-256算法 明文+秘钥+8位随机盐值 生成 hash值,生成的hash是80个十六进制的字符串,其中就包括了盐值和秘钥。

matches过程

encode的过程中,会用随机生成一个盐值,让然后用盐值+明文+秘钥的组合字符串生成hash值。那有没有想过,当用户登录的时候,怎么验证密码的正确性呢?可以确定的是,验证密码的时候不会去校验明文,因为数据库里存的是hash值,所以系统只能校验hash值。只要用户输入的明文经过hash转换后得到的hash值和数据库里的hash值一样,就说明密码正确。在上面的encode中已经提到了,盐值也在生成的hash值中。

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        byte[] digested = decode(encodedPassword);
        byte[] salt = subArray(digested, 0, saltGenerator.getKeyLength());
        return matches(digested, digest(rawPassword, salt));
    }

subArray(digested, 0, saltGenerator.getKeyLength()); 就是根据 hash值找到盐值。

上一篇 下一篇

猜你喜欢

热点阅读