生成短哈希值的哈希函数?

171

有没有一种加密方法可以将任意长度的字符串转换成小于10个字符的哈希值?我想基于消息内容生成相对独特的ID,而不是随机生成。

如果不可能处理任意长度的字符串,我可以接受将消息限制为整数值。但在这种情况下,哈希值不能对两个连续的整数太相似。


这被称为哈希。它不会是唯一的。 - SLaks
1
这也是一个哈希截断问题,因此请参见 https://dev59.com/em445IYBdhLWcg3wmLZd - Peter Krauss
2
请注意,在维基百科上可以查看哈希函数列表 - Basil Bourque
https://hashids.org/ 这个很不错,支持多种语言。例如对于golang:https://github.com/speps/go-hashids - Eric
10个回答

122
你可以使用任何常用的哈希算法(例如 SHA-1),这将给出比所需长度稍长的结果。只需截断结果至所需长度,这可能已经足够了。
例如,在Python中:
>>> import hashlib
>>> hash = hashlib.sha1("my message".encode("UTF-8")).hexdigest()
>>> hash
'104ab42f1193c336aa2cf08a2c946d5c6fd0fcdb'
>>> hash[:10]
'104ab42f11'

12
任何合理的哈希函数都可以被截断。 - President James K. Polk
140
这样做不会使碰撞的风险大大增加吗? - M Rajoy
166
使用Base64编码对于防冲突并没有帮助,因为如果hash(a)hash(b)发生碰撞,那么base64(hash(a))也会和base64(hash(b))发生碰撞。 - Greg Hewgill
71
@GregHewgill 你说得对,但我们不是在谈论原始哈希算法的冲突(是的,sha1 碰撞了,但这是另一回事)。 如果您有一个由base64编码的10个字符的哈希值,则其熵会更高,而不是使用base16(或十六进制)进行编码。 更高多少? 使用 base16 您每个字符获得4位信息, 而使用 base64 则为每个字符6位。因此,一个由10个字符组成的“hex”哈希将具有40位熵,而base64则为60位。因此,它略微更加抗性强,如果我没有表达清楚,请见谅。 - John L. Jegutanis
25
我明白你的意思了,如果你的结果有限定的大小,那么使用Base64编码可以比十六进制编码更有效地压缩更多的重要位。 - Greg Hewgill
显示剩余2条评论

62

如果您不需要一个强防意外修改的算法,我找到了一个名为adler32的算法,它可以产生相当短(~8个字符)的结果。请从这里的下拉菜单中选择该算法以尝试:

http://www.sha1-online.com/


3
非常古老,不是很可靠。 - Mascarpone
9
“@Mascarpone 'not very reliable' - source?”——来源是什么?它有一些局限性,如果你知道这些局限性,那么它的年龄就不重要了。 - B T
18
@Mascarpone "更少的弱点" - 再次,什么弱点?为什么您认为这个算法对于OP的使用不是100%完美的? - B T
5
@Mascarpone,原帖中并未表示他们需要加密级别的哈希函数。然而,Adler32是一种校验和,而不是哈希函数,因此根据原帖中实际使用情况而定,它可能不太适合。 - PM 2Ring
2
Adler32有一个重要限制,引用自Wikipedia的话:“由于这些短消息的校验和只使用32位中的少数几位,导致Adler-32在处理少量字节的短消息时存在弱点。” - Basil Bourque
显示剩余2条评论

27

你可以使用Python的hashlib库。其中,shake_128shake_256算法提供了可变长度的哈希值。以下是一些可以运行的代码(Python3):

>>> import hashlib
>>> my_string = 'hello shake'
>>> hashlib.shake_256(my_string.encode()).hexdigest(5)
'34177f6a0a'

请注意,使用长度参数x(例如5),该函数返回长度为2x的哈希值。


18

总结一下对我有帮助的回答(注意@erasmospunk的评论使用base-64编码)。我的目标是有一个短字符串,它是大多数情况下唯一的...

我不是专家,如果有任何明显错误,请纠正我(再次使用Python,就像被接受的答案一样):

import base64
import hashlib
import uuid

unique_id = uuid.uuid4()
# unique_id = UUID('8da617a7-0bd6-4cce-ae49-5d31f2a5a35f')

hash = hashlib.sha1(str(unique_id).encode("UTF-8"))
# hash.hexdigest() = '882efb0f24a03938e5898aa6b69df2038a2c3f0e'

result = base64.b64encode(hash.digest())
# result = b'iC77DySgOTjliYqmtp3yA4osPw4='

在这里,result 使用的不仅仅是十六进制字符(如果你使用 hash.hexdigest() 就会得到这个),因此它发生冲突的可能性更小(也就是说,截断后比十六进制摘要更安全)。

注意:使用UUID4(随机)。请参见http://en.wikipedia.org/wiki/Universally_unique_identifier了解其他类型。


16
您需要对内容进行哈希处理以生成摘要。有许多散列可用,但10个字符对于结果集来说相当小。很早以前,人们使用CRC-32,它产生33位散列(基本上是4个字符加一个位)。还有CRC-64,它产生65位散列。MD5用于生成128位散列(16个字节/字符),因为可以找到具有相同哈希的两个消息而被认为已经破解了加密目的。毫无疑问,每当您将任意长度的消息创建为16字节摘要时,都会出现重复项。摘要越短,冲突风险就越大。
然而,您关心哈希在两个连续消息(无论是否为整数)中不相似的问题,应该适用于所有哈希。即使原始消息中只有一个位更改,也应产生完全不同的结果摘要。
因此,使用类似于CRC-64(并对结果进行base-64编码)应该使您进入所需要的领域。

1
将SHA-1哈希进行CRC处理,然后对结果进行base-64编码,能使得生成的ID更加抗碰撞吗? - user234932
6
“然而,你对于哈希值不应该在连续的两条消息中相似的担忧……应该适用于所有哈希算法。”--这并不一定是正确的。例如,对于用于聚类或克隆检测的哈希函数,实际上恰恰相反:你希望相似的文档产生相似(甚至相同)的哈希值。一个众所周知的例子是专门设计用于产生相似输入的相同值的哈希算法 Soundex。 - Jörg W Mittag
我正在使用哈希函数来验证消息签名。因此,对于已知的消息和指定的签名,哈希必须正确。虽然会有一小部分误报,但我并不在意。这是完全可以接受的。目前我使用截断的SHA-512哈希值,压缩成Base62以方便使用(我很快就设计出来的)。 - user234932
@JörgWMittag 的观点非常好,关于 SoundEx。我承认错误了。并不是所有的哈希都具有相同的特征。 - John

14

如果您需要“子10字符哈希”,可以使用Fletcher-32算法,该算法生成8个字符的哈希值(32位),还可以使用CRC-32或Adler-32。

CRC-32比Adler32慢20%至100%。

Fletcher-32比Adler-32稍微更可靠。它的计算成本低于Adler校验和:Fletcher vs Adler comparison

下面是一个具有几个Fletcher实现的示例程序:

    #include <stdio.h>
    #include <string.h>
    #include <stdint.h> // for uint32_t

    uint32_t fletcher32_1(const uint16_t *data, size_t len)
    {
            uint32_t c0, c1;
            unsigned int i;

            for (c0 = c1 = 0; len >= 360; len -= 360) {
                    for (i = 0; i < 360; ++i) {
                            c0 = c0 + *data++;
                            c1 = c1 + c0;
                    }
                    c0 = c0 % 65535;
                    c1 = c1 % 65535;
            }
            for (i = 0; i < len; ++i) {
                    c0 = c0 + *data++;
                    c1 = c1 + c0;
            }
            c0 = c0 % 65535;
            c1 = c1 % 65535;
            return (c1 << 16 | c0);
    }

    uint32_t fletcher32_2(const uint16_t *data, size_t l)
    {
        uint32_t sum1 = 0xffff, sum2 = 0xffff;

        while (l) {
            unsigned tlen = l > 359 ? 359 : l;
            l -= tlen;
            do {
                sum2 += sum1 += *data++;
            } while (--tlen);
            sum1 = (sum1 & 0xffff) + (sum1 >> 16);
            sum2 = (sum2 & 0xffff) + (sum2 >> 16);
        }
        /* Second reduction step to reduce sums to 16 bits */
        sum1 = (sum1 & 0xffff) + (sum1 >> 16);
        sum2 = (sum2 & 0xffff) + (sum2 >> 16);
        return (sum2 << 16) | sum1;
    }

    int main()
    {
        char *str1 = "abcde";  
        char *str2 = "abcdef";

        size_t len1 = (strlen(str1)+1) / 2; //  '\0' will be used for padding 
        size_t len2 = (strlen(str2)+1) / 2; // 

        uint32_t f1 = fletcher32_1(str1,  len1);
        uint32_t f2 = fletcher32_2(str1,  len1);

        printf("%u %X \n",    f1,f1);
        printf("%u %X \n\n",  f2,f2);

        f1 = fletcher32_1(str2,  len2);
        f2 = fletcher32_2(str2,  len2);

        printf("%u %X \n",f1,f1);
        printf("%u %X \n",f2,f2);

        return 0;
    }

输出:

4031760169 F04FC729                                                                                                                                                                                                                              
4031760169 F04FC729                                                                                                                                                                                                                              

1448095018 56502D2A                                                                                                                                                                                                                              
1448095018 56502D2A                                                                                                                                                                                                                              

测试向量相符:

"abcde"  -> 4031760169 (0xF04FC729)
"abcdef" -> 1448095018 (0x56502D2A)

Adler-32对于长度只有几百字节的短消息存在弱点,因为这些消息的校验和覆盖32位中可用位数较少。请参阅:

Adler32算法不足以与可比较的校验和竞争


从技术上讲,如果它慢了100%,它永远不会结束。 - Christian
1
@Christian 我会将“100%更慢”解释为:它花费了原始时间+原始时间的100%。也就是说,它需要的时间是原来的两倍。我是少数派吗? - Ian Grainger
不,你不是。我只是那时候有点书呆子的心情。 - Christian
感谢您提供这个精确的答案,正是我所需要的。 - rpsteiger

12

现在已经是2019年,有更好的选择。即xxhash

~ echo test | xxhsum                                                           
2d7f1808da1fa63c  stdin

8
这个链接已经失效了,最好提供一个更完整的答案。 - eri0o
2
链接现在可用。 - Jon
4
为什么它更好? - Dogweather

10

在 MacOS 或 Linux 终端中运行以下命令:

crc32 <(echo "some string")

长度为8个字符。


8
您可以使用现有的哈希算法,如MD5(128位)或SHA1(160位),产生较短的结果。然后您可以通过将摘要的部分与其他部分进行XOR运算来进一步缩短长度。这会增加碰撞的机会,但不会像简单地截断摘要那样糟糕。
此外,您还可以将原始数据的长度作为结果的一部分,使其更加唯一。例如,将MD5摘要的前半部分与后半部分XOR起来将得到64位。添加32位数据的长度(如果您知道长度总是适合更少的位数,则可以更低)。这将产生一个96位(12字节)的结果,您可以将其转换为一个24个字符的十六进制字符串。或者,您可以使用Base64编码使其更短。

3
这句话的意思是“就算一点用处也没有,这被称为XOR折叠”。其中的XOR-folding需要翻译为“XOR折叠”。 - PM 2Ring

0

最近我需要一个简单的字符串缩减函数。基本上,代码看起来像这样(以下是C/C++代码):

size_t ReduceString(char *Dest, size_t DestSize, const char *Src, size_t SrcSize, bool Normalize)
{
    size_t x, x2 = 0, z = 0;

    memset(Dest, 0, DestSize);

    for (x = 0; x < SrcSize; x++)
    {
        Dest[x2] = (char)(((unsigned int)(unsigned char)Dest[x2]) * 37 + ((unsigned int)(unsigned char)Src[x]));
        x2++;

        if (x2 == DestSize - 1)
        {
            x2 = 0;
            z++;
        }
    }

    // Normalize the alphabet if it looped.
    if (z && Normalize)
    {
        unsigned char TempChr;
        y = (z > 1 ? DestSize - 1 : x2);
        for (x = 1; x < y; x++)
        {
            TempChr = ((unsigned char)Dest[x]) & 0x3F;

            if (TempChr < 10)  TempChr += '0';
            else if (TempChr < 36)  TempChr = TempChr - 10 + 'A';
            else if (TempChr < 62)  TempChr = TempChr - 36 + 'a';
            else if (TempChr == 62)  TempChr = '_';
            else  TempChr = '-';

            Dest[x] = (char)TempChr;
        }
    }

    return (SrcSize < DestSize ? SrcSize : DestSize);
}

它可能有比预期更多的碰撞,但它不是用作加密哈希函数的。如果你得到了太多的碰撞,可以尝试使用各种乘数(即将37更改为另一个质数)。这个片段的一个有趣特点是,当Src比Dest短时,Dest最终以输入字符串原样结束(0 * 37 + value = value)。如果你想在过程结束时得到一些“可读性”,Normalize将会调整转换后的字节,代价是增加碰撞。

来源:

https://github.com/cubiclesoft/cross-platform-cpp/blob/master/sync/sync_util.cpp


std::hash并不能解决某些用例(例如,当只需要几行额外的代码时,避免拖入臃肿的std::模板)。这里没有任何愚蠢之处。它经过仔细思考,以应对Mac OSX中的主要限制。我不想要一个整数。为此,我可以使用djb2,仍然避免使用std::模板。 - CubicleSoft
这仍然听起来很傻。当哈希本身如此糟糕时,为什么你会使用比4(32位)更大的DestSize?如果你想要比int输出更大的碰撞抵抗力,你应该使用SHA。 - Navin
看,它并不是一个传统的哈希。它具有有用的特性,用户可以在某些操作系统(例如Mac OSX)存在极其有限的缓冲空间的地方声明字符串大小,并且结果必须适合实际文件名的有限域,并且他们不想仅截断名称,因为那样会导致冲突(但较短的字符串保持不变)。加密哈希不总是正确的答案,std::hash也不总是正确的答案。 - CubicleSoft

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