Função de hash que produz hashes curtos?

97

Existe uma forma de criptografia que pode ter uma string de qualquer comprimento e produzir um hash de menos de 10 caracteres? Desejo produzir IDs razoavelmente exclusivos, mas com base no conteúdo da mensagem, em vez de aleatoriamente.

Posso viver restringindo as mensagens a valores inteiros, no entanto, se cadeias de comprimento arbitrário forem impossíveis. No entanto, o hash não deve ser semelhante para dois inteiros consecutivos, nesse caso.

rath3r
fonte
Isso é chamado de hash. Não será único.
SLaks
1
Este também é um problema de truncamento de hash , portanto, consulte também stackoverflow.com/q/4784335
Peter Krauss
2
Para sua informação, veja uma lista de funções hash na Wikipedia.
Basil Bourque

Respostas:

76

Você pode usar qualquer algoritmo de hash comumente disponível (por exemplo, SHA-1), que fornecerá um resultado um pouco mais longo do que o necessário. Simplesmente trunque o resultado no comprimento desejado, que pode ser bom o suficiente.

Por exemplo, em Python:

>>> import hashlib
>>> hash = hashlib.sha1("my message".encode("UTF-8")).hexdigest()
>>> hash
'104ab42f1193c336aa2cf08a2c946d5c6fd0fcdb'
>>> hash[:10]
'104ab42f11'
Greg Hewgill
fonte
2
Qualquer função hash razoável pode ser truncada.
Presidente James K. Polk
88
isso não aumentaria muito o risco de colisão?
Gabriel Sanmartin
143
@erasmospunk: a codificação com base64 não faz nada para a resistência à colisão, pois se hash(a)colide com, hash(b)então base64(hash(a))também colide com base64(hash(b)).
Greg Hewgill
56
@GregHewgill você está certo, mas não estamos falando sobre a colisão do algoritmo de hash original (sim, sha1colide , mas esta é outra história). Se você tiver um hash de 10 caracteres, você obtém uma entropia maior se estiver codificado com base64vsbase16 (ou hex). Quão mais alto? Com base16você obtém 4 bits de informação por caractere, base64este valor é 6 bits / char. No total, um hash "hex" de 10 caracteres terá 40 bits de entropia, enquanto um hash base64 de 60 bits. Então é um pouco mais resistente, desculpe se não fui super claro.
John L. Jegutanis
19
@erasmospunk: Oh, entendo o que você quer dizer, sim, se você tiver um tamanho fixo limitado para o seu resultado, você pode compactar bits mais significativos com codificação base64 vs. codificação hex.
Greg Hewgill
46

Se você não precisa de um algoritmo que seja forte contra modificação intencional, encontrei um algoritmo chamado adler32 que produz resultados bastante curtos (~ 8 caracteres). Escolha na lista suspensa aqui para experimentar:

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

BT
fonte
2
é muito antigo, não é muito confiável.
Mascarpone
1
@Mascarpone "não muito confiável" - fonte? Tem limitações, se você os conhece não importa quantos anos tenha.
BT de
8
@Mascarpone "menos fraquezas" - novamente, quais são as fraquezas? Por que você acha que este algoritmo não é 100% perfeito para o uso do OP?
BT
3
@Mascarpone O OP não diz que eles querem um hash criptográfico. OTOH, Adler32 é uma soma de verificação, não um hash, então pode não ser adequado, dependendo do que o OP está realmente fazendo com ele.
PM 2Ring
2
Há uma ressalva para Adler32, citando Wikipedia : Adler-32 tem um ponto fraco para mensagens curtas com algumas centenas de bytes, porque as somas de verificação dessas mensagens têm uma cobertura insuficiente dos 32 bits disponíveis.
Basil Bourque
13

Você precisa hash o conteúdo para fazer um resumo. Existem muitos hashes disponíveis, mas 10 caracteres é muito pequeno para o conjunto de resultados. Há muito tempo, as pessoas usavam CRC-32, que produz um hash de 33 bits (basicamente 4 caracteres mais um bit). Também existe o CRC-64 que produz um hash de 65 bits. MD5, que produz um hash de 128 bits (16 bytes / caracteres), é considerado quebrado para fins criptográficos porque duas mensagens podem ser encontradas com o mesmo hash. Não é preciso dizer que sempre que você criar um resumo de 16 bytes a partir de uma mensagem de comprimento arbitrário, acabará com duplicatas. Quanto mais curto for o resumo, maior será o risco de colisões.

No entanto, sua preocupação de que o hash não seja semelhante para duas mensagens consecutivas (inteiros ou não) deve ser verdadeira com todos os hashes. Mesmo uma única mudança de bit na mensagem original deve produzir um resumo resultante muito diferente.

Portanto, usar algo como CRC-64 (e base-64 para obter o resultado) deve levá-lo ao bairro que está procurando.

John
fonte
1
O CRC de um hash SHA-1 e, em seguida, a base de 64 do resultado torna o ID resultante mais resistente à colisão?
5
"No entanto, sua preocupação de que o hash não seja semelhante por duas mensagens consecutivas [...] deve ser verdadeira com todos os hashes." - Isso não é necessariamente verdade. Por exemplo, para funções hash que são usadas para agrupamento ou detecção de clones, o oposto é verdadeiro, na verdade: você deseja que documentos semelhantes produzam valores hash semelhantes (ou até os mesmos). Um exemplo conhecido de um algoritmo hash projetado especificamente para produzir valores idênticos para entradas semelhantes é o Soundex.
Jörg W Mittag
Estou usando os hashes para autenticar a assinatura da mensagem. Então, basicamente, para uma mensagem conhecida e assinatura especificada, o hash deve estar correto. Eu não me importo se haveria uma pequena porcentagem de falsos positivos, no entanto. É totalmente aceitável. Atualmente, uso o hash SHA-512 truncado compactado com base62 (algo que criei rapidamente) por conveniência.
@ JörgWMittag Excelente ponto no SoundEx. Eu estou corrigido. Nem todos os hashes têm as mesmas características.
John
12

Apenas resumindo uma resposta que foi útil para mim (observando o comentário de @erasmospunk sobre o uso da codificação em base 64). Meu objetivo era ter uma pequena cadeia que foi principalmente único ...

Não sou nenhum especialista, então corrija se houver algum erro gritante (em Python novamente como a resposta aceita):

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='

O resultaqui está usando mais do que apenas caracteres hexadecimais (o que você obteria se os usasse hash.hexdigest()), então é menos provável que haja uma colisão (ou seja, deve ser mais seguro truncar do que um resumo hexadecimal).

Nota: Usando UUID4 (aleatório). Consulte http://en.wikipedia.org/wiki/Universally_unique_identifier para os outros tipos.

JJ Geewax
fonte
7

Você poderia usar um algoritmo hash existente que produza algo curto, como MD5 (128 bits) ou SHA1 (160). Em seguida, você pode encurtar ainda mais por XORing seções do resumo com outras seções. Isso aumentará a chance de colisões, mas não tão ruim quanto simplesmente truncar o resumo.

Além disso, você pode incluir o comprimento dos dados originais como parte do resultado para torná-los mais exclusivos. Por exemplo, fazer o XOR da primeira metade de um resumo MD5 com a segunda metade resultaria em 64 bits. Adicione 32 bits para o comprimento dos dados (ou menos se você souber que o comprimento sempre caberá em menos bits). Isso resultaria em um resultado de 96 bits (12 bytes) que você poderia transformar em uma string hexadecimal de 24 caracteres. Como alternativa, você pode usar a codificação base 64 para torná-la ainda mais curta.

Dynamichael
fonte
2
FWIW, isso é conhecido como dobradura XOR.
PM 2Ring
7

Se você precisar, "sub-10-character hash" pode usar o algoritmo Fletcher-32 que produz hash de 8 caracteres (32 bits), CRC-32 ou Adler-32 .

CRC-32 é mais lento que Adler32 por um fator de 20% - 100%.

Fletcher-32 é ligeiramente mais confiável do que Adler-32. Ele tem um custo computacional menor do que a soma de verificação de Adler: comparação Fletcher vs Adler .

Um programa de amostra com algumas implementações do Fletcher é fornecido abaixo:

    #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;
    }

Resultado:

4031760169 F04FC729                                                                                                                                                                                                                              
4031760169 F04FC729                                                                                                                                                                                                                              

1448095018 56502D2A                                                                                                                                                                                                                              
1448095018 56502D2A                                                                                                                                                                                                                              

Concorda com os vetores de teste :

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

O Adler-32 tem um ponto fraco para mensagens curtas com poucas centenas de bytes, porque as somas de verificação dessas mensagens têm uma cobertura insuficiente dos 32 bits disponíveis. Verifique isto:

O algoritmo Adler32 não é complexo o suficiente para competir com somas de verificação comparáveis .

sg7
fonte
6

Basta executá-lo em um terminal (no MacOS ou Linux):

crc32 <(echo "some string")

8 caracteres de comprimento.

sgon00
fonte
4

Você pode usar a biblioteca hashlib para Python. Os shake_128 e shake_256 algoritmos fornecer hash de comprimento variável. Aqui estão alguns códigos de trabalho (Python3):

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

Observe que, com um parâmetro de comprimento x (5 no exemplo), a função retorna um valor hash de comprimento 2x .

Feran
fonte
1

Agora é 2019 e existem opções melhores. Ou seja, xxhash .

~ echo test | xxhsum                                                           
2d7f1808da1fa63c  stdin
sorvete
fonte
Este link está quebrado. é melhor fornecer uma resposta mais completa.
eri0o
0

Eu precisava de algo semelhante a uma função simples de redução de string recentemente. Basicamente, o código se parecia com isto (código C / C ++ adiante):

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);
}

Provavelmente tem mais colisões do que o desejado, mas não se destina ao uso como uma função hash criptográfica. Você pode tentar vários multiplicadores (ou seja, mudar o 37 para outro número primo) se houver muitas colisões. Um dos recursos interessantes desse trecho é que quando Src é menor que Dest, Dest termina com a string de entrada como está (0 * 37 + valor = valor). Se você quiser algo "legível" no final do processo, o Normalize ajustará os bytes transformados ao custo de aumentar as colisões.

Fonte:

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

CubicleSoft
fonte
std :: hash não resolve certos casos de uso (por exemplo, evitar arrastar os std :: templates inchados quando apenas algumas linhas extras de código serão suficientes). Não há nada de bobo aqui. Ele foi cuidadosamente pensado para lidar com as principais limitações do Mac OSX. Eu não queria um número inteiro. Para isso, eu poderia ter usado djb2 e ainda evitado usar std :: templates.
CubicleSoft
Isso ainda parece bobo. Por que você sempre use um DestSizemaior do que 4 (32 bits) quando o próprio hash é tão ínfima? Se você quisesse a resistência à colisão fornecida por uma saída maior que um int, você usaria SHA.
Navin
Olha, não é realmente um hash tradicional. Ele tem propriedades úteis onde o usuário pode declarar o tamanho da string em lugares onde há espaço de buffer extremamente limitado em certos sistemas operacionais (por exemplo, Mac OSX) E o resultado deve caber no domínio limitado de nomes de arquivos reais E eles não querem apenas truncar o nome porque isso IRIA causar colisões (mas strings mais curtas são deixadas sozinhas). Um hash criptográfico nem sempre é a resposta certa e std :: hash também nem sempre é a resposta certa.
CubicleSoft