Hash加密算法

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

密码安全

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

  • 明文存储
    即用户输入的是什么密码,就存储什么密码。
  • Hash存储
    Hash存储的意思是:对用户输入的密码按照Hash算法得到Hash值,然后将Hash值存到数据库中。
    为什么说是 "Hash存储",而不是 "加密存储"?因为这本来就不算是加密解密的过程,而且很容易对人造成误解。Hash算法是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要),而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。

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

  • MD5是输入不定长度信息,输出固定长度128-bits的算法。经过程序流程,生成四个32位数据,最后联合起来成为一个128-bits散列。基本方式为,求余、取余、调整长度、与链接变量进行循环运算。得出结果。

  • SHA-1在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5(更早之前被广为使用的散列函数)的后继者。

  • SHA-2它的算法跟SHA-1基本上仍然相似;因此有些人开始发展其他替代的散列算法。

  • 由于对MD5出现成功的破解,以及对SHA-0和SHA-1出现理论上破解的方法,NIST感觉需要一个与之前算法不同的,可替换的加密杂凑算法,也就是现在的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值找到盐值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容