C ++ Crypto: Część 1 - Hash
Rozglądanie się za nowoczesnymi bibliotekami Crypto.
Nie udało się znaleźć nic dobrego.
Wiem, że prawdopodobnie zrobiłem to wszystko źle, więc pracuj ze mną tutaj. Będą cztery różne przeglądy czterech struktur, które wzajemnie się uzupełniają:
- Haszowanie
- Hashed Key
- Klucz hasła
- Odpowiedź na solone wyzwanie
To jest kod mieszający i zapewnia proste opakowanie wokół SHA-1 i SHA-256, ale wzorzec jest na tyle prosty, że możemy go rozszerzyć na inne mechanizmy mieszania.
Struktury danych i implementacja przedstawione w tych pytaniach są oparte na RFC2104 i tym poście w codeproject .
Przykład użycia:
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
Odpowiedzi
Właściwa kapitalizacja SHA1
Algorytm nazywa się SHA1, a nie Sha1, więc myślę, że lepiej jest użyć tutaj wszystkich wielkich liter. To sprawia, że grepowanie kodu dla określonego algorytmu jest łatwiejsze.
Potrzebujesz tylko jednej klasy na algorytm skrótu
Rzeczywiście, jak wspomniałeś w swojej własnej odpowiedzi, Sha1klasa wydaje się zbędna, ponieważ nie przechowuje żadnego stanu. Jednak zamiast tworzyć funkcje statyczne w Sha1przestrzeni nazw, można utworzyć te funkcje jako funkcje składowe klasy, która przechowuje rzeczywisty stan. Pozwala to uniknąć powtarzania typu; na przykład:
Sha1::DigestStore digest;
Sha1::hash("Bob", digest)
Staje się:
Sha1::DigestStore digest;
digest.hash("Bob");
Sha1::Digest vs. Digest<Sha1>
Myślę, że posiadanie przestrzeni nazw Sha1zawierającej DigestStorefunkcje ai to zły wybór. Z SHA1 możesz zrobić więcej niż tylko tworzyć zwykłe skróty, na przykład możesz chcieć utworzyć HMAC zamiast zwykłego skrótu. Musiałbyś więc dodać funkcje, aby utworzyć HMAC do każdej przestrzeni nazw, która implementuje algorytm skrótu. Jest o wiele lepiej mieć zajęcia Digesti HMACże są na matrycy o algorytm skrótu.
Zezwalaj na aktualizację skrótów
Kod, który napisałeś, wykonuje tylko jednorazowe konwersje niektórych danych wejściowych na skrót. Jednak nierzadko zdarza się, że programy nie mają wszystkich danych, dla których chcą utworzyć skrót, w jednym, ciągłym regionie pamięci. W takich przypadkach chcesz napisać:
std::ostream output;
Digest<SHA1> digest;
digest.add("Header");
digest.add("Data");
digest.add("Footer");
output << digest.view();
Niektóre algorytmy skrótu mogą wymagać wywołania jakiejś funkcji w celu obliczenia ostatecznej wartości skrótu po dodaniu wszystkich danych. Możesz dodać jawną finish()funkcję lub wywołać ją niejawnie podczas uzyskiwania dostępu do wyniku skrótu.
Wydobywanie wyniku
Przechowujesz wewnętrznie hash jako plik std::array<std::byte, size>. To właściwa rzecz. Nie sądzę, aby konieczne było zapewnienie jakichkolwiek funkcji składowych innych niż ta, która zapewnia constodniesienie do tej tablicy. To dzwoniący musi przekonwertować go na dowolną formę. A std::arrayjest już niejawnie konwertowany na plik std::span. A kiedy już masz odniesienie do tablicy, łatwo jest uzyskać z niej iteratory początku i końca.
Dodaj operatory porównania
To dość powszechne, aby chcieć sprawdzić, czy dwa skróty są identyczne, więc dobrze byłoby przynajmniej określić operator==()i operator!=()do klasy, która posiada wynik strawienia.
Myślę, że zmienię interfejs.
Obecnie wzór użycia to:
typename Sha1::DigestStore digest;
Sha1 hasher;
hasher.hash("Bob", digest);
Nie ma potrzeby tworzenia Sha1obiektu. Myślę, że lepszym interfejsem może być wykonanie wszystkich metod, staticaby użycie stało się:
typename Sha1::DigestStore digest;
Sha1::hash("Bob", digest);
DigestStoreMoże potrzebować kilka innych funkcji dostępów. Obecnie pozwala, iterationale może istnieć przypadek użycia, w którym mamy plik const_iterator.
Wciąż próbuję zrozumieć, kiedy najlepiej użyć string_view. Niestety nadal nie gra dobrze ze zwykłymi strunami. Więc być może będziemy musieli zapewnić sposób na wyodrębnienie łańcucha z bufora. W takim przypadku byłoby miło, gdybyśmy mogli umieścić dane z DigestStore w łańcuchu (co oznacza, że nie używamy std::array), ale potrzebujemy dobrego przypadku użycia, aby to działało lepiej.
Nie wiem jeszcze, jak to zadziała. Podaj podpowiedź, jeśli masz pomysł.