编写高质量代码改善C#程序的157个建议——建议114:MD5不再安全

建议114:MD5不再安全

MD5不再安全不是就算法本身而言的。如果从可逆性的角度出发,MD5值不存在被破解的可能性。

MD5被广泛应用于密码验证和消息完整性验证。假设新注册一个用户,当注册用户的密码第一次被存储到数据库时,往往会将其转换为MD5值存储:

        static string GetMd5Hash(string input)
        {
            using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
            {
                return BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(input))).Replace("-", "");
            }
        }

        static void Main(string[] args)
        {
            string source = "liming's key";
            string hash = GetMd5Hash(source);
            Console.WriteLine("保存密码原文:{0}的MD5值:{1}到数据库。",source,hash);

            Console.Read();
        }

输出为:

保存密码原文:liming's key的MD5值:B222558FD330454B08878C61FD595121到数据库。

如果MD5值存储在数据库中,当用户登录时,只需要验证MD5就可以检查用户输入的密码是否正确。如下:

        static void Main(string[] args)
        {
            Console.WriteLine("请输入密码,按回车键结束……");
            string source = Console.ReadLine();
            if (VerifyMd5Hash(source, "B222558FD330454B08878C61FD595121"))
            {
                Console.WriteLine("密码正确,准许登录系统。");
            }
            else
            {
                Console.WriteLine("密码有误,拒绝登录。");
            }
        }    

        static bool VerifyMd5Hash(string input, string hash)
        {
            string hashOfInput = GetMd5Hash(input);
            StringComparer comparer = StringComparer.OrdinalIgnoreCase;
            return comparer.Compare(hashOfInput, hash) == 0 ? true : false;
        }

输出为:

请输入密码,按回车键结束……

liming's key

密码正确,准许登录系统。

处于隐私保护的目的,所以不直接存储密码。即便是一个银行系统,我们也不想让银行的后台管理人员看到我们的密码。而通过MD5值来校验,就可以确保无人可以查看或破解我们的密码,也达到了密码验证的目的。虽然有人可能会质疑,MD5的算法不是多对一的吗?也就是说,可能存在一个另外的密码,求出来的MD5值和我这个密码是一样的啊。但是,在实际应用场合中,这个概率会很小,小到可以忽略不计。

既然到目前为止所说的都是MD5的优点,那么,为什么说MD5是不安全的呢?因为,这个世界上还有一种方法叫做穷举法。由于用户安全意识先对薄弱,所以他们设置的密码很有可能是简单的数字集合。这种情况下如果破解密码。穷举法会派上很大的用处。以密码“8888”为例,测试下我们用穷举法破解所需时间:

        static void Main(string[] args)
        {

            Console.WriteLine("开始穷举法破解用户密码……");
            string key = string.Empty;
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i < 9999; i++)
            {
                if (VerifyMd5Hash(i.ToString(), "CF79AE6ADDBA60AD018347359BD144D2"))
                {
                    key = i.ToString();
                    break;
                }
            }
            watch.Stop();
            Console.WriteLine("密码已破解,为:{0},耗时{1}毫秒。", key, watch.ElapsedMilliseconds);
        }

输出为:

开始穷举法破解用户密码……

密码已破解,为:8888,耗时124毫秒。

可见,如果我们的密码过于简单,计算机甚至都不需要1秒的时间就能完成暴力破解。当然,这种算法不是针对MD5的可逆破解,而是非常愚蠢的穷举。现在,已经有很多免费的商业的MD5字典库,存储了相当数量字符串的MD5值,我们只要提交一个MD5值进去,立刻就可以得到他的原文,只要这个原文不是非常复杂。所以,从这方面来说,MD5不再安全。

因此,我们需要找一个办法来改进MD5求值了。目前,最通用的算法是多次使用MD5值发。我们修改一下GetMd5Hash方法,代码如下:

        static string GetMd5Hash(string input)
        {
            string hashKey = "Aa1@#$,.Klj+{>.45oP";
            using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
            {
                string hashCode = BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(input))).Replace("-", "") + BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(hashKey))).Replace("-", "");
                return BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(hashCode))).Replace("-", "");
            }
        }

在改进后的方法中,我们首先设计了一个足够复杂的密码hashKey,然后将它的MD5值和用户输入密码的MD5值相加,再求一次MD5值作为返回值。经过这个过程以后,密码的长度就够了,复杂度也够了,要想通过穷举法来得到真正的密码成本也就大大增加了。

转自:《编写高质量代码改善C#程序的157个建议》陆敏技