存储哈希密码 - base64、十六进制字符串或其他什么?

17

我正在使用.NET System.Security.Cryptography类进行密码散列。它有几个散列算法,例如MD5、SHA1、SHA256、SHA384和SHA512。

产生的散列值是一个字节数组。我应该将其转换为十六进制字符串进行存储,还是使用Convert.ToBase64String(),或者其他的方法?(我更喜欢Base64,因为它比Hex短)

顺便说一句,由于有这么多散列算法可供选择,我随机选择了SHA384,但是否有更“好”的或适合该任务的算法呢?

请评论。

阅读前八条评论后更新:
根据答案和进一步阅读,似乎MD5、SHA1差不多(SHA1稍微更安全)。SHA256、384、512提供了越来越好的安全性。

由于我不需要强加纽瓦克(这是用于内部企业系统的,没有url、浏览器、internet、intranet或extranet),我将跳过“加盐”业务——我想,如果有人能窃取密码表,他们也可能窃取其他表中的实际数据。

但我会保留“盐”的概念以备将来参考;我不确定盐应该在密码哈希之前附加(在前面)还是在哈希之后附加(在末尾),是否会有所不同?此外,我考虑使用密码本身的前几个字符作为盐,以避免额外的字段来存储它,但我想这长度不够——盐应该足够长。

共识认为Base64转换是一种合理的存储和比较选择。对于哈希存储,我需要弄清楚最大数据库列长度是什么,给定最大密码长度为15个字符。也许是Varchar(64)?

感谢大家的贡献。


字段的长度取决于您选择的哈希算法,而不是密码的长度。没有理由限制密码的长度或可用字符。对于SHA384,它将是384/6个字符(64),因为每个base64字符表示6位。 - Bell
为什么不用安全的方式来练习呢? :) 按照以下步骤进行:
  1. 创建一个常数128位全局盐,存储在你的.NET应用程序中;
  2. 创建一个128位本地盐,每个密码都是独立的,并存储在DB中的旁边;
  3. 计算一个在1000到1500之间的本地数字N,并将其存储在DB中,旁边有每个密码;
  4. 在全局盐前面添加本地盐,并使用SHA512的N次迭代进行哈希。将此存储在DB中。
保证是一种安全的方法来存储您的密码。 :D
- Vilx-
2
安全地持久化密码真的非常容易。没有不正确地实现的借口,特别是“我不需要堡垒”。根据数据库系统和将要使用的编程语言,您可以决定将盐和哈希存储为字节数组(在SQL Server中为BINARY-对于SHA512,您将需要一个长度为64的BINARY列用于哈希)。 - yfeldblum
1
给你的哈希加盐比选择SHA256而不是MD5更有意义。如果你不加盐,反向查找将变得更容易(简单的谷歌搜索可以在毫秒内带来结果),如果有人获得了密码数据库,他至少可以知道有哪些人使用了相同的密码,并开始破解这些密码。因此,首先使用盐,然后再考虑破解SHA256与SHA512的概率。 (也就是说,请忘记MD5) - Yann Schwartz
10个回答

12
即使您的解决方案不是铁桶一样安全,也应该勤奋地实施盐加密。因为许多人会在其他地方重复使用他们的密码,如果攻击者选择将破解的密码数据库用于其他目的,入侵将对您的组织外部造成额外的损害。
盐加密使字典攻击变得更加昂贵。通过决定使用什么盐大小,您可以微调您的机会。以下是Bruce Schneier的"Applied Cryptography"中的引用:
“盐并非万能药;增加盐位数并不能解决所有问题。盐只能保护密码文件免受常规字典攻击,而无法防范针对单个密码的有计划攻击。它可以保护那些在多台机器上使用相同密码的人,但并不能使选择不当的密码更好。”
以下是C#的示例。这并不难。您可以选择要使用的盐大小和哈希函数。免责声明:如果您真的关心密码完整性,请使用bcrypt之类的东西。

using System;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;

public class PassHash {
    private static readonly RandomNumberGenerator rng = RandomNumberGenerator.Create();
    public static readonly int DefaultSaltSize = 8; // 64-bit salt
    public readonly byte[] Salt;
    public readonly byte[] Passhash;

    internal PassHash(byte[] salt, byte[] passhash) {
        Salt = salt;
        Passhash = passhash;
    }

    public override String ToString() {
        return String.Format("{{'salt': '{0}', 'passhash': '{1}'}}",
                             Convert.ToBase64String(Salt),
                             Convert.ToBase64String(Passhash));
    }

    public static PassHash Encode<HA>(String password) where HA : HashAlgorithm {
        return Encode<HA>(password, DefaultSaltSize);
    }

    public static PassHash Encode<HA>(String password, int saltSize) where HA : HashAlgorithm {
        return Encode<HA>(password, GenerateSalt(saltSize));
    }

    private static PassHash Encode<HA>(string password, byte[] salt) where HA : HashAlgorithm {
        BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static;
        MethodInfo hasher_factory = typeof (HA).GetMethod("Create", publicStatic, Type.DefaultBinder, Type.EmptyTypes, null);
        using (HashAlgorithm hasher = (HashAlgorithm) hasher_factory.Invoke(null, null))
        {
            using (MemoryStream hashInput = new MemoryStream())
            {
                hashInput.Write(salt, 0, salt.Length);
                byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
                hashInput.Write(passwordBytes, 0, passwordBytes.Length);
                hashInput.Seek(0, SeekOrigin.Begin);
                byte[] passhash = hasher.ComputeHash(hashInput);
                return new PassHash(salt, passhash);
            }
        }
    }

    private static byte[] GenerateSalt(int saltSize) {
        // This generates salt.
        // Rephrasing Schneier:
        // "salt" is a random string of bytes that is
        // combined with password bytes before being
        // operated by the one-way function.
        byte[] salt = new byte[saltSize];
        rng.GetBytes(salt);
        return salt;
    }

    public static bool Verify<HA>(string password, byte[] salt, byte[] passhash) where HA : HashAlgorithm {
        // OMG: I don't know how to compare byte arrays in C#.
        return Encode<HA>(password, salt).ToString() == new PassHash(salt, passhash).ToString();
    }
}

用法:

新用户提交其凭据。

PassHash ph = PassHash.Encode<SHA384>(new_user_password);

ph.Saltph.Passhash存储在某个地方...稍后,当用户再次登录时,您查找具有salt和passhash的用户记录,然后执行以下操作:

PassHash.Verify<SHA384>(user_login_password, user_rec.salt, user_rec.passhash)

}


给我端上来的代码?我喜欢这样 :-) 但是我已经完成了大部分。 - joedotnot
2
要比较字节数组,您可以执行以下操作: ba1.SequenceEqual(ba2); // 我喜欢Linq - Talljoe
+1 Talljoe,我是恐龙。当我不得不完全用Python编程时,我擅长C# 2.0和任何.NET FCL版本。自那以后,好事情发生在C#中,例如LINQ。 - Pavel Repin

8

Base64比十六进制确实更短,但由于涉及到符号,如果它最终出现在URL中,您需要小心。基本上这取决于您在哪里使用它。十六进制通常具有易于肉眼“阅读”的优势,但由于这将是无意义的数据,因此对于哈希来说并不相关。(如果必要,您始终可以使用在线base64解码器。)

当然,这一切都是假设您想要一个字符串。与不透明的字节数组相比,字符串易于存储、传输等。


4

顺便提一下,有这么多哈希算法可供选择,我随机选择了SHA384,但是否有更好的算法或适合此任务的算法呢?

总的来说,我会避免使用MD5、SHA-0或SHA-1。但目前来看,SHA-384应该已经足够满足您的需求。请参见this question以获取有关算法的讨论。


4

有趣的一点是,如果您有一个字节数组,并对其进行base64编码,则某些结果字符可能会在url中引起问题-但是,如果您再次对其进行base64编码,则这些有问题的字符往往会被删除,并且可以将base64字符串用于url。您可以比较使用双重编码的字符串。

在哈希算法方面,sha384可能是足够的算法,但请确保对哈希进行盐处理!


这是一篇关于“加盐哈希”的非常信息丰富的文章。在自己开发密码哈希/存储方案之前,请先阅读它。http://www.matasano.com/log/958/enough-with-the-rainbow-tables-what-you-need-to-know-about-secure-password-schemes/ - Pavel Repin
为什么我要解码两次?我不能只对提供的密码进行哈希处理,然后将其编码两次,再将其与已经编码两次的存储哈希值进行比较吗? - joedotnot
非常正确,抱歉,我过去使用这种方法需要将字节内容取回 - 已适当编辑。 - Alistair Evans
2
hex(string) 比 base64(base64(string)) 仅多占用一点点空间(12.5%),更易于理解,不区分大小写,并且不包含任何需要在 URL 中转义的字符。 - yfeldblum

4
为了回答你问题的第二部分,SHA384 可能有些过头了。SHA1 可以完全满足你的需求,但是它存在一个理论上的漏洞(与能够创建一个篡改文档以散列到相同值的漏洞有关,不用担心)。
如果你还没有这样做,请确保在对密码进行哈希之前对其进行盐处理,以防止更常见的黑客攻击(识别相似密码、彩虹表等)。阅读此 article 以获取更多信息。
顺便说一下,我喜欢 Base64 ,因为 .NET 具有内置的转换例程。

1
为什么SHA-1漏洞“不需要担心”?如果有人可以制造一个与我的密码哈希值相同的密码,那就等同于破解了我的密码。对于密码,我不会使用低于SHA-256的算法。 - Graeme Perrow
更正:我不会使用低于SHA-256的加密算法。 - Graeme Perrow
1
据我所知,目前的理论漏洞是基于已有的明文;也就是说——创建一个恶意版本的明文。找到一个散列为给定值的明文仍然非常困难。如果您已经拥有密码,创建一个恶意版本的密码也没有什么用处。 - Talljoe

3

您还可以将其转换为纯字节。因此,128位MD5哈希值将导致长度为128位/8位/字节= 16字节的字符串。


这个想法通常是正确的 - 字节可以很容易地存储为字节。然而,这可能会使手动操作有些困难,因为使用编辑器/IDE/工具通常不可能以任何合理的格式查看二进制数据。 - Sander
我不确定为什么这个答案被投票否决。如果散列密码存储在诸如MySQL和SQL Server之类的数据库中,两者(以及我愿意打赌的许多其他数据库)都支持固定长度的二进制字段,并且这将是存储和比较哈希值最有效的方式。该问题没有明确提到结果必须存储为字符字符串。 - Jason Musgrove
@Sander -- 什么?这正是你首先需要一个 IDE 的原因! - Christoffer
为什么要操作哈希值?即使需要,也不必向用户显示哈希值的二进制数据。但是对于存储来说,二进制数据将是最佳选择。 - Gumbo
你会如何存储这些二进制数据?请记住,哈希值是一个字节数组,例如{1,2,3,4,...,48},其中每个值1、2、3都可以是0-255之间的任意数字。我需要将其转换为字符串以便在一个字段中进行存储。而且,这个字符串越短,比较起来就越有效率。 - joedotnot
每个元素将代表一个字节。我不了解.NET,但可能有一个函数可以将您的整数值转换为字符。也许http://msdn.microsoft.com/library/system.byte.tostring.aspx可以实现这一点。 - Gumbo

0

使用只包含字母和数字的Base36或Base62表示法如何?(可以安全地在URL中传输)。我只是在Java中尝试了以下内容

BigInteger hexNumber = new BigInteger(hexString, 16);
// Convert it to Base 36 yields smaller string than hexString
hexNumber.toString(36);

将Base36数转换回十六进制

BigInteger b36String= new BigInteger(b36String, 36);
// Convert it to Base 16 yields original hex string
hexNumber.toString(16);

顺便提一下,Java的BigInteger最大基数值为36。

对于Base62,我们可以使用一个包含62个字符(26个小写字母、26个大写字母和10个数字)的查找表,通过将十六进制数除以64并保留余数,不断附加从查找表中检索到的字符来实现。

欢迎提出建议。


0

对于密码,我相信任何一个都可以完美地服务。 SHA 512 将创建一个更高的密钥,降低"冲突"的机会,因为不同的密码可能有相同的哈希值,但概率非常低。


0
答案取决于上下文和约束条件。与十六进制编码相比,Base64将更紧凑,但生成时会稍微计算复杂一些。
您应该向哈希数据添加盐。这是将随机值放在您要哈希的密码前面。然后存储盐值和哈希值,以便您可以重新生成哈希值。
这种盐的作用是防止建立将常见密码映射到哈希值的字典。如果有人能够获得您的哈希密码文件的副本,则可以使用此字典查找明文密码。对于相同的明文密码,使用16位盐可以获得65536个不同的哈希值。字典攻击变得不太有效。最好选择64位盐,因为它不会增加太多的哈希和验证密码的工作量。
您还应该添加一个幻数(常量字节序列)与哈希值。攻击者将需要知道此幻数才能生成适当的哈希值。因此,如果攻击者只拥有哈希密码文件,则无法继续进行攻击。他将需要获取最有可能深藏在您代码中的魔术单词。
关于算法的选择,使用SHA1非常好,使用SHA256更为保险。MD5计算成本要低得多,并且可以满足大多数用例。如果您的处理和存储资源可能成为限制因素,请考虑使用MD5。

1
为什么你说要把盐放在密码前面?为什么不放在后面?这会有什么区别吗? - joedotnot
2
是的,这会有一点不同。哈希函数逐字节处理输入字节,并且可以保存和恢复中间状态。如果附加盐,则无需重新哈希整个输入字符串即可获取哈希值。您只需从中间状态开始即可。当盐放在前面时,必须重新哈希整个输入字符串,这需要更多的计算力量来进行暴力猜测。 - chmike

0

我会使用Base64...哦,如果你要哈希密码的话,别忘了加盐 :) 如果我们谈论密码,任何比SHA384小的算法都存在安全风险,因为对于SHA1和其他常见哈希算法来说,获取高质量彩虹表是非常容易的。


我认为任何低于SHA-256的加密方式都存在安全风险。 - Graeme Perrow
https://crackstation.net/ 和 http://www.sha1-online.com/。我测试了从SHA-1到SHA-512的“alphalphaalpha”,并将它们全部反转了。这意味着非盐值密码现在几乎没有用处了。 - Brenden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接