C ++ Crypto: Parte 1- Hash

Aug 26 2020

Procurando por bibliotecas Crypto modernas.
Não foi possível encontrar nada de bom.

Eu sei que provavelmente fiz tudo errado, então trabalhe comigo aqui. Haverá quatro análises diferentes para quatro estruturas construídas umas sobre as outras:

  1. Hashing
  2. Hashed Key
  3. Chave de senha
  4. Salted Challenge Response

Este é o código de hash e fornece um invólucro simples em torno de SHA-1 e SHA-256, mas o padrão é simples o suficiente para que possamos expandi-lo para outros mecanismos de hash.

As estruturas de dados e a implementação apresentadas nessas questões são baseadas na RFC2104 e nesta postagem no codeproject .

Exemplo de uso:

DigestStore<Sha1>    hash;    // <- destination of hash
Sha1                 hasher;

hasher.hash("This string can be hashsed", hash);

hash.h

#ifndef THORS_ANVIL_CRYPTO_HASH_H
#define THORS_ANVIL_CRYPTO_HASH_H

#ifdef  __APPLE__
#define COMMON_DIGEST_FOR_OPENSSL
#include <CommonCrypto/CommonDigest.h>
#define THOR_SHA1(data, len, dst)       CC_SHA1(data, len, dst)
#define THOR_SHA256(data, len, dst)     CC_SHA256(data, len, dst)
#else
#include <openssl/sha.h>
#define THOR_SHA1(data, len, dst)       SHA1(data, len, dst)
#define THOR_SHA256(data, len, dst)     SHA256(data, len, dst)
#endif

#include <string>
#include <array>

//
// Wrapper for sha1 and sha256 hashing algorithms
//
// Provides a simple wrapper class with the appropriates types and size
// for the resulting "digest" object. Also provides several type safe
// versions of the hashing algorithm to allow multiple know types to
// be safely hashed.

namespace ThorsAnvil::Crypto
{

using Byte      = char unsigned;
using DigestPtr = Byte*;

template<typename Hash>
using Digest = typename Hash::DigestStore;

template<std::size_t size>
class DigestStore
{
    std::array<Byte, size>      data;
    public:
        using iterator = typename std::array<Byte, size>::iterator;

        operator Digest()                           {return &data[0];}
        std::string_view  view()                    {return std::string_view(reinterpret_cast<char const*>(&data[0]), std::size(data));}
        Byte&             operator[](std::size_t i) {return data[i];}
        iterator          begin()                   {return std::begin(data);}
        iterator          end()                     {return std::end(data);}
};

// These versions of the hashing function are good for hashing short
// amounts of text. Use these for passwords and validation hashes
// do not use them for hashing large documents.
struct Sha1
{
    static constexpr std::size_t digestSize = SHA_DIGEST_LENGTH;
    using DigestStore = DigestStore<SHA_DIGEST_LENGTH>;

    void hash(DigestStore& src,             DigestStore& dst)   {THOR_SHA1(src, SHA_DIGEST_LENGTH, dst);}
    void hash(std::string_view src,         DigestStore& dst)   {THOR_SHA1(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
    void hash(std::string const& src,       DigestStore& dst)   {THOR_SHA1(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
    // Use only if you know the destination is large enough!!
    void hashUnsafe(std::string_view src,   DigestPtr dst)      {THOR_SHA1(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
};
struct Sha256
{
    static constexpr std::size_t digestSize = SHA256_DIGEST_LENGTH;
    using DigestStore = DigestStore<SHA256_DIGEST_LENGTH>;

    void hash(DigestStore& src,             DigestStore& dst)   {THOR_SHA256(src, SHA256_DIGEST_LENGTH, dst);}
    void hash(std::string_view src,         DigestStore& dst)   {THOR_SHA256(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
    void hash(std::string const& src,       DigestStore& dst)   {THOR_SHA256(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
    // Use only if you know the destination is large enough!
    void hashUnsafe(std::string_view src,   Digestptr dst)      {THOR_SHA256(reinterpret_cast<Byte const*>(&src[0]), std::size(src), dst);}
};

}

#endif

Respostas

3 G.Sliepen Aug 27 2020 at 03:58

Capitalização adequada de SHA1

O nome do algoritmo é SHA1, não Sha1, então acho melhor usar maiúsculas aqui. Isso torna o grepping do código para um algoritmo específico mais fácil.

Você só precisa de uma classe por algoritmo de hash

Na verdade, como você mencionou em sua própria resposta, a Sha1classe parece supérflua, pois não armazena nenhum estado. No entanto, em vez de criar funções estáticas dentro de um Sha1namespace, você pode tornar essas funções funções-membro da classe que contém o estado real. Isso evita repetir o tipo; por exemplo:

Sha1::DigestStore digest;
Sha1::hash("Bob", digest)

Torna-se:

Sha1::DigestStore digest;
digest.hash("Bob");

Sha1::Digest vs. Digest<Sha1>

Acho que ter um namespace Sha1com DigestStorefunções e dentro dele é uma escolha ruim. Você pode fazer mais com SHA1 do que apenas criar hashes simples, por exemplo, você pode querer criar um HMAC em vez de um hash simples. Portanto, você teria que adicionar funções para criar um HMAC para cada namespace que implementa um algoritmo de hash. É muito melhor ter aulas Digeste HMACque são modeladas no algoritmo de hash.

Permitir que hashes sejam atualizados

O código que você escreveu realiza apenas conversões one-shot de alguma entrada para um hash. No entanto, não é incomum que os programas não tenham todos os dados para os quais desejam criar um hash em uma única região de memória contígua. Nesses casos, você deseja escrever:

std::ostream output;
Digest<SHA1> digest;

digest.add("Header");
digest.add("Data");
digest.add("Footer");

output << digest.view();

Alguns algoritmos de resumo podem exigir que você chame alguma função para calcular o valor de hash final após adicionar todos os dados. Você pode adicionar uma finish()função explícita ou chamá-la implicitamente ao acessar o resultado do resumo.

Obtendo o resultado

Você armazena internamente o hash como um std::array<std::byte, size>. Essa é a coisa certa a fazer. Eu não acho que seja necessário fornecer quaisquer funções-membro, exceto aquela que fornece uma constreferência a esse array. Cabe ao chamador convertê-lo em qualquer forma que desejar. A std::arrayjá é implicitamente conversível em a std::span. E, uma vez que você tenha uma referência ao array, é fácil obter os iteradores de início e fim dele.

Adicionar operadores de comparação

É muito comum querer verificar se dois hashes são idênticos, então seria útil pelo menos definir operator==()e operator!=()para a classe que contém o resultado do resumo.

1 MartinYork Aug 26 2020 at 05:27

Acho que vou mudar a interface.

Atualmente, o padrão de uso é:

typename Sha1::DigestStore      digest;
Sha1                            hasher;

hasher.hash("Bob", digest);

Não parece necessário criar um Sha1objeto. Acho que uma interface melhor seria fazer todos os métodos staticpara que o uso se tornasse :

typename Sha1::DigestStore      digest;

Sha1::hash("Bob", digest);

O DigestStorepode precisar de algumas outras funções de acesso. Atualmente permite, iterationmas pode haver um caso de uso em que temos um const_iterator.

Ainda tentando entender quando é melhor usar string_view. Infelizmente, ele ainda não funciona bem com cordas normais. Portanto, talvez tenhamos que fornecer uma maneira de também extrair uma string do buffer. Nesse caso, seria bom se pudéssemos ter os dados do DigestStore em uma string (o que significa não usar std::array), mas precisamos ter um bom caso de uso para fazer isso funcionar melhor.

Não tenho certeza de como isso vai funcionar ainda. Por favor, dê uma dica se você tiver uma ideia.