摘要:

1 加Salt散列
2 ASP.NET 2.0 Membership中与密码散列有关的代码

声明:本文所罗列之源代码均通过Reflector取自.NET Framework类库,Anders Liu引用这些代码仅出于学习和研究的目的。

前一段关于密码的存储问题产生了一些讨论。我所看到的景象是,首先在cnbeta新闻中提到中国某银行将强制冻结密码过于简单(如6个8)的帐户,引发了争论。一方认为银行采用明文存放用户密码;另一方则认为,即便密码是经过散列存放的,但只要得到“6个8”的散列值,通过对比散列值也可以发现具有特定密码的用户。

后来在博客园(cnblogs.com)也看到有朋友发帖讨论了密码的散列存储,再后来在MVP的QQ群里也就这个问题小小议论了一番。

其实,对密码进行散列存储不是一个新鲜话题了,解决起来也不是很难,但很多人还是不大了解。Anders Liu这个小文只是强调一下“加Salt散列”这个简单的技术,并给出ASP.NET Membership所使用的代码。

本来打算写一篇介绍如何实现用户登录功能的文章的,但因为时间有限,所以先介绍一下密码的散列,下一篇再介绍用户登录。

----

1 密码必须散列存储

(内容略)

2 加Salt散列

我们知道,如果直接对密码进行散列,那么黑客(统称那些有能力窃取用户数据并企图得到用户密码的人)可以对一个已知密码进行散列,然后通过对比散列值得到某用户的密码。换句话说,虽然黑客不能取得某特定用户的密码,但他可以知道使用特定密码的用户有哪些。

加Salt可以一定程度上解决这一问题。所谓加Salt,就是加点“佐料”。其基本想法是这样的——当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。

这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。

下面详细介绍一下加Salt散列的过程。介绍之前先强调一点,前面说过,验证密码时要使用和最初散列密码时使用“相同的”佐料。所以Salt值是要存放在数据库里的。


图1. 用户注册

如图1所示,注册时,

1)用户提供密码(以及其他用户信息);
2)系统为用户生成Salt值;
3)系统将Salt值和用户密码连接到一起;
4)对连接后的值进行散列,得到Hash值;
5)将Hash值和Salt值分别放到数据库中。


图2. 用户登录

如图2所示,登录时,

1)用户提供用户名和密码;
2)系统通过用户名找到与之对应的Hash值和Salt值;
3)系统将Salt值和用户提供的密码连接到一起;
4)对连接后的值进行散列,得到Hash'(注意有个“撇”);
5)比较Hash和Hash'是否相等,相等则表示密码正确,否则表示密码错误。

3 ASP.NET 2.0 Membership中的相关代码

(省略关于Membership的介绍若干字)

本文Anders Liu仅研究了SqlMembershipProvider,该类位于System.Web.dll,System.Web.Security命名空间中。

首先,要使用Membership,必须先用aspnet_regsql.exe命令来配置数据库,该工具会向现有数据库中添加一系列表和存储过程等,配置好的数据库中有一个表aspnet_Membership,就是用于存放用户帐户信息的。其中我们所关注的列有三个——Password、PasswordFormat和PasswordSalt。

Password存放的是密码的散列值,PasswordFormat存放用于散列密码所使用的算法,PasswordSalt就是系统生成的Salt值了。

注册时用到了该类的CreateUser方法,该方法主要代码如下:

CreateUser
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
    string str3;
    MembershipUser user;
    if (!SecUtility.ValidateParameter(ref password, true, true, false, 0x80))
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    // 生成salt值
    string salt = base.GenerateSalt();
    // 结合salt值对密码进行散列
    string objValue = base.EncodePassword(password, (int) this._PasswordFormat, salt);
    if (objValue.Length > 0x80)
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    if (passwordAnswer != null)
    {
        passwordAnswer = passwordAnswer.Trim();
    }
    if (!string.IsNullOrEmpty(passwordAnswer))
    {
        if (passwordAnswer.Length > 0x80)
        {
            status = MembershipCreateStatus.InvalidAnswer;
            return null;
        }
        str3 = base.EncodePassword(passwordAnswer.ToLower(CultureInfo.InvariantCulture), (int) this._PasswordFormat, salt);
    }
    else
    {
        str3 = passwordAnswer;
    }
    if (!SecUtility.ValidateParameter(ref str3, this.RequiresQuestionAndAnswer, true, false, 0x80))
    {
        status = MembershipCreateStatus.InvalidAnswer;
        return null;
    }
    if (!SecUtility.ValidateParameter(ref username, true, true, true, 0x100))
    {
        status = MembershipCreateStatus.InvalidUserName;
        return null;
    }
    if (!SecUtility.ValidateParameter(ref email, this.RequiresUniqueEmail, this.RequiresUniqueEmail, false, 0x100))
    {
        status = MembershipCreateStatus.InvalidEmail;
        return null;
    }
    if (!SecUtility.ValidateParameter(ref passwordQuestion, this.RequiresQuestionAndAnswer, true, false, 0x100))
    {
        status = MembershipCreateStatus.InvalidQuestion;
        return null;
    }
    if ((providerUserKey != null) && !(providerUserKey is Guid))
    {
        status = MembershipCreateStatus.InvalidProviderUserKey;
        return null;
    }
    if (password.Length < this.MinRequiredPasswordLength)
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    int num = 0;
    for (int i = 0; i < password.Length; i++)
    {
        if (!char.IsLetterOrDigit(password, i))
        {
            num++;
        }
    }
    if (num < this.MinRequiredNonAlphanumericCharacters)
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    if ((this.PasswordStrengthRegularExpression.Length > 0) && !Regex.IsMatch(password, this.PasswordStrengthRegularExpression))
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    ValidatePasswordEventArgs e = new ValidatePasswordEventArgs(username, password, true);
    this.OnValidatingPassword(e);
    if (e.Cancel)
    {
        status = MembershipCreateStatus.InvalidPassword;
        return null;
    }
    try
    {
        SqlConnectionHolder connection = null;
        try
        {
            connection = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true);
            this.CheckSchemaVersion(connection.Connection);
            DateTime time = this.RoundToSeconds(DateTime.UtcNow);
            SqlCommand command = new SqlCommand("dbo.aspnet_Membership_CreateUser", connection.Connection);
            command.CommandTimeout = this.CommandTimeout;
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add(this.CreateInputParam("@ApplicationName", SqlDbType.NVarChar, this.ApplicationName));
            command.Parameters.Add(this.CreateInputParam("@UserName", SqlDbType.NVarChar, username));
            command.Parameters.Add(this.CreateInputParam("@Password", SqlDbType.NVarChar, objValue));
            command.Parameters.Add(this.CreateInputParam("@PasswordSalt", SqlDbType.NVarChar, salt));
            command.Parameters.Add(this.CreateInputParam("@Email", SqlDbType.NVarChar, email));
            command.Parameters.Add(this.CreateInputParam("@PasswordQuestion", SqlDbType.NVarChar, passwordQuestion));
            command.Parameters.Add(this.CreateInputParam("@PasswordAnswer", SqlDbType.NVarChar, str3));
            command.Parameters.Add(this.CreateInputParam("@IsApproved", SqlDbType.Bit, isApproved));
            command.Parameters.Add(this.CreateInputParam("@UniqueEmail", SqlDbType.Int, this.RequiresUniqueEmail ? 1 : 0));
            command.Parameters.Add(this.CreateInputParam("@PasswordFormat", SqlDbType.Int, (int) this.PasswordFormat));
            command.Parameters.Add(this.CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, time));
            SqlParameter parameter = this.CreateInputParam("@UserId", SqlDbType.UniqueIdentifier, providerUserKey);
            parameter.Direction = ParameterDirection.InputOutput;
            command.Parameters.Add(parameter);
            parameter = new SqlParameter("@ReturnValue", SqlDbType.Int);
            parameter.Direction = ParameterDirection.ReturnValue;
            command.Parameters.Add(parameter);
            command.ExecuteNonQuery();
            int num3 = (parameter.Value != null) ? ((int) parameter.Value) : -1;
            if ((num3 < 0) || (num3 > 11))
            {
                num3 = 11;
            }
            status = (MembershipCreateStatus) num3;
            if (num3 != 0)
            {
                return null;
            }
            providerUserKey = new Guid(command.Parameters["@UserId"].Value.ToString());
            time = time.ToLocalTime();
            user = new MembershipUser(this.Name, username, providerUserKey, email, passwordQuestion, null, isApproved, false, time, time, time, time, new DateTime(0x6da, 1, 1));
        }
        finally
        {
            if (connection != null)
            {
                connection.Close();
                connection = null;
            }
        }
    }
    catch
    {
        throw;
    }
    return user;
}

其中我们可以看到两个比较令人感兴趣的方法:GenerateSalt和EncodePassword。由于本文讨论的仅仅是密码的散列,而不是整个用户注册过程,所以这里只对这两个函数进行分析。

这两个方法来自于SqlMembershipProvider的父类,MembershipProvider。

GenerateSalt方法的代码比较简单:

GenerateSalt
internal string GenerateSalt()
{
    byte[] data = new byte[0x10];
    new RNGCryptoServiceProvider().GetBytes(data);
    return Convert.ToBase64String(data);
}

但是要注意的是,在这种方法里Salt值的高度随机性是安全的保障,所以不能简单的使用Random来获取随机数,而应该使用更安全的方式。这里使用了RNGCryptoServiceProvider来生成随机数。

EncodePassword方法的代码也不难:

EncodePassword
internal string EncodePassword(string pass, int passwordFormat, string salt)
{
    if (passwordFormat == 0)
    {
        return pass;
    }
    // 将密码和salt值转换成字节形式并连接起来
    byte[] bytes = Encoding.Unicode.GetBytes(pass);
    byte[] src = Convert.FromBase64String(salt);
    byte[] dst = new byte[src.Length + bytes.Length];
    byte[] inArray = null;
    Buffer.BlockCopy(src, 0, dst, 0, src.Length);
    Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
    // 选择算法,对连接后的值进行散列
    if (passwordFormat == 1)
    {
        HashAlgorithm algorithm = HashAlgorithm.Create(Membership.HashAlgorithmType);
        if ((algorithm == null) && Membership.IsHashAlgorithmFromMembershipConfig)
        {
            RuntimeConfig.GetAppConfig().Membership.ThrowHashAlgorithmException();
        }
        inArray = algorithm.ComputeHash(dst);
    }
    else
    {
        inArray = this.EncryptPassword(dst);
    }
    // 以字符串形式返回散列值
    return Convert.ToBase64String(inArray);
}

这段代码的作用就是,首先将密码和salt值转换成字节数组(分别放到bytes和src数组中),然后拼接到一起(dst数组)。之后再根据Web.config中设置的加密算法,对这个拼接值进行散列,最后把散列值转换成字符串形式返回。

最后,用户登录时,将会使用SqlMembershipProvider的CheckPassword方法对密码进行检验。该方法有两种重载形式,最为完整的一种如下所示:

CheckPassword
private bool CheckPassword(string username, string password, bool updateLastLoginActivityDate, bool failIfNotApproved, out string salt, out int passwordFormat)
{
    SqlConnectionHolder connection = null;
    string str;  // 密码散列值
    int num;
    int num2;
    int num3;
    bool flag2;
    DateTime time;
    DateTime time2;
    // 从数据库中拿到Hash和Salt
    this.GetPasswordWithFormat(username, updateLastLoginActivityDate, out num, out str, out passwordFormat, out salt, out num2, out num3, out flag2, out time, out time2);
    if (num != 0)
    {
        return false;
    }
    if (!flag2 && failIfNotApproved)
    {
        return false;
    }
    // 对用户刚刚输入的密码进行散列
    string str2 = base.EncodePassword(password, passwordFormat, salt);

    // 比较两个散列值,看密码是否相等
    bool objValue = str.Equals(str2);
    if ((objValue && (num2 == 0)) && (num3 == 0))
    {
        return true;
    }
    try
    {
        try
        {
            connection = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true);
            this.CheckSchemaVersion(connection.Connection);
            SqlCommand command = new SqlCommand("dbo.aspnet_Membership_UpdateUserInfo", connection.Connection);
            DateTime utcNow = DateTime.UtcNow;
            command.CommandTimeout = this.CommandTimeout;
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add(this.CreateInputParam("@ApplicationName", SqlDbType.NVarChar, this.ApplicationName));
            command.Parameters.Add(this.CreateInputParam("@UserName", SqlDbType.NVarChar, username));
            command.Parameters.Add(this.CreateInputParam("@IsPasswordCorrect", SqlDbType.Bit, objValue));
            command.Parameters.Add(this.CreateInputParam("@UpdateLastLoginActivityDate", SqlDbType.Bit, updateLastLoginActivityDate));
            command.Parameters.Add(this.CreateInputParam("@MaxInvalidPasswordAttempts", SqlDbType.Int, this.MaxInvalidPasswordAttempts));
            command.Parameters.Add(this.CreateInputParam("@PasswordAttemptWindow", SqlDbType.Int, this.PasswordAttemptWindow));
            command.Parameters.Add(this.CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, utcNow));
            command.Parameters.Add(this.CreateInputParam("@LastLoginDate", SqlDbType.DateTime, objValue ? utcNow : time));
            command.Parameters.Add(this.CreateInputParam("@LastActivityDate", SqlDbType.DateTime, objValue ? utcNow : time2));
            SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int);
            parameter.Direction = ParameterDirection.ReturnValue;
            command.Parameters.Add(parameter);
            command.ExecuteNonQuery();
            num = (parameter.Value != null) ? ((int) parameter.Value) : -1;
            return objValue;
        }
        finally
        {
            if (connection != null)
            {
                connection.Close();
                connection = null;
            }
        }
    }
    catch
    {
        throw;
    }
    return objValue;
}

这个代码首先通过GetPasswordWithFormat得到了Hash值(变量str)和Salt值(变量salt),然后对用户输入的密码(参数password)进行与注册时一样的散列(只是salt值使用了数据库中现存的值)得到散列值str2,之后通过对比str和str2,就知道密码正确与否了。

4 小结

本文只是简单地介绍了加Salt散列的工作方式(而非原理)、ASP.NET 在Membership中对其的实现。通过本文大家虽然无法对加Salt加密的有点和原理“知其所以然”,但相信大家应该大致了解了这种方式的使用方法,并能通过修改Membership的代码实现自己的密码散列存储了。

由于时间有限,Anders Liu这篇文章写得很潦草,罗列了不少代码却没有系统性介绍,还望大家原谅。下一篇文章我将相对完整地介绍如何实现自己的用户登录(无需使用MembershipProvider,但同时也丧失了Login等控件为我们带来的便利)。

转载于:https://www.cnblogs.com/AndersLiu/archive/2007/12/28/encode-password-with-salt.html

浅析ASP.NET 2.0的用户密码加密机制相关推荐

  1. Edusoho修改注册的用户密码加密机制规则

    一.简介 1.修改生成$salt的机制规则. 2.修改生成$password的机制规则. 二.edusoho的默认用户密码加密机制规则 1.系统默认生成$salt的方式: edusoho\src\Bi ...

  2. salt盐度与用户密码加密机制

    1 加Salt散列 2 ASP.NET 2.0 Membership中与密码散列有关的代码 声明:本文所罗列之源代码均通过Reflector取自.NET Framework类库,引用这些代码仅出于学习 ...

  3. C#中使用MD5对用户密码加密与解密

    C#中常涉及到对用户密码的加密于解密的算法,其中使用MD5加密是最常见的的实现方式.本文总结了通用的算法并结合了自己的一点小经验,分享给大家. 一.使用16位.32位.64位MD5方法对用户名加密 1 ...

  4. 使用MD5对用户密码加密与解密

    MD5简介 : MD5的全称是Message-Digest Algorithm 5,在90年代初由MIT的计算机科学实验室和RSA Data Security Inc发明,经MD2.MD3和MD4发展 ...

  5. php+$2y$10,PHP 用户密码加密函数password_hash

    PHP 用户密码加密函数password_hash PHP 用户密码加密函数password_hash 传统的用户名和密码都采用加盐的方式存储加密信息,盐值也需要存储. 自PHP5.5.0之后,新增加 ...

  6. 用户密码加密存储十问十答,一文说透密码安全存储

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | 程序员赵鑫 来源 | cnblogs.com/xinzh ...

  7. [转]常见的用户密码加密方式以及破解方法

    [作者]张辉,就职于携程技术中心信息安全部,负责安全产品的设计与研发. 作为互联网公司的信息安全从业人员经常要处理撞库扫号事件,产生撞库扫号的根本原因是一些企业发生了信息泄露事件,且这些泄露数据未加密 ...

  8. 新增用户-用户密码加密-无解密

    新增用户-用户密码加密 加密方式 需求 做法 加密方式 加密方式有多种,如1加密后可解密得到原文得.2加密后无解密方式,只能通过加密密文比对得.本文采取得就是第2种无解密方式加密 需求 springb ...

  9. 常见的用户密码加密及破解方法

    一.用户密码加密 用户密码保存到数据库时,常见的加密方式有哪些,我们该采用什么方式来保护用户的密码呢?以下几种方式是常见的密码保存方式: ① 直接明文保存,比如用户设置的密码是"123456 ...

最新文章

  1. Typora入门(2)
  2. linux c size_t ssize_t 简介
  3. Oracle 密码文件
  4. 数据库语法_圣诞快乐:用GaussDB T 绘制一颗圣诞树,兼论高斯数据库语法兼容...
  5. Github 热榜项目:如何让你的终端酷炫到没朋友
  6. 直接拿来用,10个PHP代码片段(收藏)
  7. 上市开放式基金(LOF)
  8. conda deactivate python3_conda进行python环境隔离
  9. 360天擎默认卸载密码_用好360(四)
  10. SpringSocial业务系统与社交网站的绑定与解绑
  11. Android SurfaceFlinger 学习之路(五)----VSync 工作原理
  12. Srs之HttpApi内部调用流程
  13. 干货 | 我可以读哪些论文来跟上现代NLP的最新趋势?
  14. Java 9 : 从零开始实现模块化(一)
  15. PIP安装本地离线包whl
  16. Android Automotive车载嵌入式系统
  17. “spoolsv.exe应用程序错误”的解决方法
  18. 刚刚随便GOOGLE和BAIDU了下PIPO和BLOG
  19. eNSP上华为路由器开SNMP
  20. transformer预测过程_Transformer在推荐模型中的应用总结

热门文章

  1. 挤爆了!故宫首次晚间开放:预约票平台一度502
  2. 程序员:像机器一样思考
  3. 从H264/H265码流中获取宽、高及帧率
  4. SDL 1.2.14在windows平台下的编译及例子
  5. Eclipse安装lombook
  6. 为什么我的modbus tcp server只能连一个client_TCP 协议概览
  7. 【Flink】Disconnect from JobManager responsible for
  8. 80-10-010-原理-Java NIO-简介
  9. 【kafka】kafka 报错 no brokers found when trying to rebalance
  10. Java中的JsonConfig详解