Conversation
601b85b to
0404f62
Compare
0404f62 to
efc3a2c
Compare
reneme
left a comment
There was a problem hiding this comment.
Thanks for this example to help moving the Keccak discussion forward. I believe that makes your point clearer. To my mind the concept of Keccak_c and MDx_Hash is very similar.
Nevertheless, I'm still trying to wrap my head around why this inheritance was a problem. Is there a technical reason that I'm still missing?
For the sake of discussion, let me use this PR as a base for an alternative suggestion. Don't get me wrong: I'm fine with it either way. I just got hooked by the design space and would like to get to the bottom of it. 😇
| template<MD_Endian ENDIAN, | ||
| typename DIGEST_T, | ||
| size_t DIGEST_ELEM, | ||
| void init_fn(DIGEST_T[DIGEST_ELEM]), | ||
| void compress_fn(DIGEST_T[DIGEST_ELEM], const uint8_t[], size_t), | ||
| size_t BLOCK_BYTES = 64, | ||
| size_t DIGEST_LENGTH = DIGEST_ELEM * sizeof(DIGEST_T), | ||
| size_t CTR_BYTES = 8> | ||
| class MD_Hash final |
There was a problem hiding this comment.
Let me suggest an alternative design using C++20 concepts and a CRTP-ish structure: Instead of this long list of parameters, we could write a concept to tie down the parameters each MDx-hash needs to define (as static constexpr members). Then, the instantiation of MDx_Hash in the concrete implementations shrinks to: MDx_Hash<MD5> m_md;:
template <typename T>
concept mdx_hash_implementation =
requires(T::digest_type digest, uint8_t input[], size_t blocks, MD_Endian endian) {
typename T::digest_type;
T::endianness;
T::block_bytes;
T::ctr_bytes;
T::init(digest);
T::compress_n(digest, input, blocks);
} &&
T::block_bytes >= 64 && is_power_of_2(T::block_bytes) &&
T::ctr_bytes >= 8 && is_power_of_2(T::ctr_bytes) &&
T::ctr_bytes < T::block_bytes &&
sizeof(typename T::digest_type) >= 16;
template <mdx_hash_implementation T>
class MDx
{
std::array<uint8_t, T::block_bytes> m_buffer;
T::digest_type m_digest;
uint64_t m_count;
size_t m_position;
};
class MD5
{
public:
using digest_type = std::array<uint32_t, 4>;
static constexpr MD_Endian endianness = MD_Endian::Little;
static constexpr size_t block_bytes = 64;
static constexpr size_t ctr_bytes = 8;
public:
static void init(digest_type& digest);
static void compress_n(digest_type& digest, const uint8_t input[], size_t blocks);
private:
MDx<MD5> m_md;
};That surely is a bit more boilerplate in the concrete implementations. But I see a few advantages:
- It lends itself from the usual STL members: e.g.
std::vector<>::value_type - Readability: Instead of a long list of magic numbers in the
MDx_Hashinstantiation, each parameter has a speaking name - DRY: Parameter definitions can be referred to in the implementation classes (if needed) -> less implicit knowledge: e.g.
digest_typeencapsulates thedigest_lengthanddigest_element_typeand theinit()/compress_n()methods don't need to repeat it - Parameter constraints are declared as a concept, hopefully resulting in better compiler errors
Certainly this can be further refined.
Godbolt: https://godbolt.org/z/1h1nzcYoh
| void MD5::add_data(const uint8_t input[], size_t length) | ||
| { | ||
| m_md.add_data(input, length); | ||
| } | ||
|
|
||
| void MD5::final_result(uint8_t output[]) | ||
| { | ||
| m_md.final_result(output); | ||
| } | ||
|
|
||
| /* | ||
| * Clear memory of sensitive data | ||
| */ | ||
| void MD5::clear() | ||
| { | ||
| MDx_HashFunction::clear(); | ||
| zeroise(m_M); | ||
| m_digest[0] = 0x67452301; | ||
| m_digest[1] = 0xEFCDAB89; | ||
| m_digest[2] = 0x98BADCFE; | ||
| m_digest[3] = 0x10325476; | ||
| m_md.clear(); | ||
| } | ||
|
|
||
| std::unique_ptr<HashFunction> MD5::new_object() const | ||
| { | ||
| return std::make_unique<MD5>(); | ||
| } | ||
|
|
||
| std::unique_ptr<HashFunction> MD5::copy_state() const | ||
| { | ||
| return std::make_unique<MD5>(*this); | ||
| } |
There was a problem hiding this comment.
Now that each MDx hash inherits straight from HashFunction, these "plumbing methods" are repeated for each class. From what I can see, they all simply forward to their respective m_md or create a copy of themselves. Its just copy-and-paste boilerplate, but it does bug me somewhat.
With the full CRTP-approach I suggested earlier this would go away. At the expense of inheriting from an MDx template.
| MD_Hash<MD_Endian::Big, uint64_t, 16, | ||
| SHA_384::init, SHA_512::compress_digest, 128, 48, 16> m_md; |
There was a problem hiding this comment.
Is that correct? The compress and init methods take [8] elements.
| MD_Hash<MD_Endian::Big, uint64_t, 16, | |
| SHA_384::init, SHA_512::compress_digest, 128, 48, 16> m_md; | |
| MD_Hash<MD_Endian::Big, uint64_t, 8, | |
| SHA_384::init, SHA_512::compress_digest, 128, 48, 16> m_md; |
Do I misunderstand something or is that a copy-and-paste error? (also for the SHA-flavours in this file)
|
For reference as promised: #3553. I've taken this patch as a starting point and moved common stuff into a CRTP base class. What do you think? |
|
This needs some consideration:
I'm willing to take this over. But would suggest to put it on hold until #3681 is merged. Edit: Let's also wait for #3693. |
|
Closing in favor of #3705 |
No description provided.