From d2fccb0b912bd060561cdf4ae98dc4186d09babe Mon Sep 17 00:00:00 2001 From: dd86k Date: Thu, 23 Dec 2021 13:52:12 -0500 Subject: [PATCH] Init! --- .github/SECURITY.md | 8 + .github/workflows/d.yaml | 18 ++ .gitignore | 16 ++ LICENSE | 23 ++ README.md | 76 +++++++ dub.sdl | 5 + source/blake2d.d | 452 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 598 insertions(+) create mode 100644 .github/SECURITY.md create mode 100644 .github/workflows/d.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dub.sdl create mode 100644 source/blake2d.d diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..00355df --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,8 @@ +# Security Policy + +Please note that I only support the last version of this module. + +If you believe to have found a vulnerability in blake2-d, I strongly advise you +to draft a new security advisory under Security > Securitiy advisories. + +If that's not alright with you, you can always send me an email (found on my profile). \ No newline at end of file diff --git a/.github/workflows/d.yaml b/.github/workflows/d.yaml new file mode 100644 index 0000000..974a390 --- /dev/null +++ b/.github/workflows/d.yaml @@ -0,0 +1,18 @@ +name: D + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: dlang-community/setup-dlang@v1 + - name: 'Build' + run: dub build --compiler=dmd + - name: 'Test' + run: dub test --compiler=dmd \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccabdd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.dub +dub.selections.json +docs.json +__dummy.html +docs/ +/blake2-d +blake2-d.so +blake2-d.dylib +blake2-d.dll +blake2-d.a +blake2-d.lib +blake2-d-test-* +*.exe +*.o +*.obj +*.lst diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..127a5bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a70a52 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# blake2-d + +BLAKE2 library written in D implementing the BLAKE2b and BLAKE2s hashing +algorithms and is compatible with the Phobos Digest API (std.digest). + +BLAKE2 was introduced in 2015 as IETF RFC 7693. You can visit +[the website](https://www.blake2.net/) for more information. + +This module: + +- [x] Supports BLAKE2b-512 and BLAKE2s-256. +- [x] Supports custom hash sizes. +- [ ] Supports HMAC. (WIP!) + +Compatible and tested with DMD, GDC, and LDC. + +Pull Requests accepted. + +**If you would like to disclose a vulnerability, please consult [SECURITY.md](../master/.github/SECURITY.md).** + +# Usage + +To include it in your project, simply import the `blake2d` package. + +## Digest API + +If you are unfamiliar with the Digest API, here is a quick summary. + +Two APIs are available: Template API and OOP API. + +### Template API + +The template API uses a structure template and is a good choice if your +application only plans to support one digest algorithm. + +```d +// NOTE: hexString is from std.conv +BLAKE2b512 b2b512; +b2b512.put("abc"); +assert(b2b512.finish() == cast(ubyte[]) hexString!( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"~ + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923" + )); +b2b512.start(); // reset +b2b512.put("abcdef"); +assert(b2b512.finish() == cast(ubyte[]) hexString!( + "dde410524e3569b303e494aa82a3afb3e426f9df24c1398e9ff87aafbc2f5b7b"~ + "3c1a4c9400409de3b45d37a00e5eae2a93cc9c4a108b00f05217d41a424d2b8a"); +``` + +### OOP API + +The OOP API uses a class (object) implementation and is a good choice if +your application plans to support one or more digest algorithms. + +```d +// NOTE: hexString is from std.conv +Digest dgst = new BLAKE2b512Digest(); +dgst.put("abc"); +assert(dgst.finish() == cast(ubyte[]) hexString!( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"~ + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923" + )); +dgst.start(); // reset +dgst.put("abcdef"); +assert(dgst.finish() == cast(ubyte[]) hexString!( + "dde410524e3569b303e494aa82a3afb3e426f9df24c1398e9ff87aafbc2f5b7b"~ + "3c1a4c9400409de3b45d37a00e5eae2a93cc9c4a108b00f05217d41a424d2b8a"); +``` + +There are numerous ways to avoid GC allocation. For example when only using a +digest for a one-time use in a short scope, there's `std.typecons.scoped`. + +# License + +Published under the Boost License 1.0. \ No newline at end of file diff --git a/dub.sdl b/dub.sdl new file mode 100644 index 0000000..a78b5f1 --- /dev/null +++ b/dub.sdl @@ -0,0 +1,5 @@ +name "blake2-d" +description "BLAKE2s and BLAKEb implementations." +authors "dd86k " +copyright "Copyright © 2021, dd86k" +license "BSL-1.0" diff --git a/source/blake2d.d b/source/blake2d.d new file mode 100644 index 0000000..d55c021 --- /dev/null +++ b/source/blake2d.d @@ -0,0 +1,452 @@ +/// Computes BLAKE2b and BLAKE2s hashes of arbitary data. +/// Reference: IETF RFC 7693 +/// License: $(LINK2 www.boost.org/LICENSE_1_0.txt, Boost License 1.0) +/// Authors: $(LINK2 github.com/dd86k, dd86k) +module blake2d; + +private import std.digest; +private import core.bitop : ror, bswap; + +// "For BLAKE2b, the two extra permutations for rounds 10 and 11 are +// SIGMA[10..11] = SIGMA[0..1]." +private immutable ubyte[16][12] SIGMA = [ + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ], + [ 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, ], + [ 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, ], + [ 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, ], + [ 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, ], + [ 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, ], + [ 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, ], + [ 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, ], + [ 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, ], + [ 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, ], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ], + [ 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, ], +]; + +private +immutable ulong[8] B2B_IV = [ + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179 +]; + +private +immutable uint[8] B2S_IV = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 +]; + +/// Used with the BLAKE2 structure template to make the BLAKE2s and BLAKE2p +/// aliases. +enum BLAKE2Variant { + b, /// BLAKE2b (default) + s, /// BLAKE2s +} + +/// BLAKE2 structure template. +/// +/// It is recommended to use the BLAKE2p512 and BLAKE2s256 aliases. +/// However, if you wish to use a custom digest size, this is the structure +/// to use. +/// +/// Example definitions for BLAKE2s-160: +/// --- +/// alias BLAKE2s160 = BLAKE2!(BLAKE2Variant.s, 160); +/// auto blake2s160_Of(T...)(T data) { return digest!(BLAKE2s160, T)(data); } +/// public alias BLAKE2s160Digest = WrapperDigest!BLAKE2s160; +/// --- +/// Params: +/// var = BLAKE2 hash variation. +/// digestSize = Digest size in bits. +// key = HMAC key. This is a temporary hack to allow HMAC usage. +struct BLAKE2(BLAKE2Variant var, uint digestSize/*, const(ubyte)[] key = null*/) +{ + @safe: + @nogc: + nothrow: + pure: + + static assert(digestSize > 0, + "Digest size must be non-zero."); + static assert(digestSize % 8 == 0, + "Digest size must be dividable by 8."); + + static if (var == BLAKE2Variant.b) { // BLAKE2b + // 8 to 512 bits + static assert(digestSize >= 8 && digestSize <= 512, + "BLAKE2b digest size must be between 8 and 512 bits."); + + private enum MAXED = digestSize == 512; + private enum BSIZE = 128; /// bb + private enum ROUNDS = 12; + private enum R1 = 32; + private enum R2 = 24; + private enum R3 = 16; + private enum R4 = 63; + alias IV = B2B_IV; + alias inner_t = ulong; + } else static if (var == BLAKE2Variant.s) { // BLAKE2s + // 8 to 256 bits + static assert(digestSize >= 8 && digestSize <= 256, + "BLAKE2s digest size must be between 8 and 256 bits."); + + private enum MAXED = digestSize == 256; + private enum BSIZE = 64; /// bb + private enum ROUNDS = 10; + private enum R1 = 16; + private enum R2 = 12; + private enum R3 = 8; + private enum R4 = 7; + alias IV = B2S_IV; + alias inner_t = uint; + } else static assert(0, "Invalid BLAKE2 variant."); + + enum blockSize = digestSize; /// Digest size in bits + + /// Initiate or reset the state of the structure. + void start() + { + this = typeof(this).init; + } + + /// Feed the algorithm with data. + /// Also implements the $(REF isOutputRange, std,range,primitives) + /// interface for `ubyte` and `const(ubyte)[]`. + /// Params: input = Input data to digest + void put(scope const(ubyte)[] input...) @trusted + { + version (BLAKE2Trace) writefln("put() len=%u", input.length); + + // Process wordwise if properly aligned. + if ((c | cast(size_t) input.ptr) % size_t.alignof == 0) + { + foreach (const word; (cast(size_t*) input.ptr)[0 .. input.length / size_t.sizeof]) + { + if (c >= BSIZE) + { + t[0] += c; + if (t[0] < c) ++t[1]; // Overflow + compress(); + c = 0; + } + mz.ptr[c / size_t.sizeof] = word; + c += size_t.sizeof; + } + input = input.ptr[input.length - (input.length % size_t.sizeof) .. input.length]; + } + + // Process remainder bytewise. + foreach (const i; input) + { + if (c >= BSIZE) + { + t[0] += c; + if (t[0] < c) ++t[1]; // Overflow + compress(); + c = 0; + } + m8[c++] = i; + } + } + + /// Returns the finished hash. + /// Returns: Raw digest data. + ubyte[digestSizeBytes] finish() + { + // final counter update + t[0] += c; + if (t[0] < c) ++t[1]; + + // 0-pad message buffer + m8[c..$] = 0; + last = 1; + compress(); + + // Clear out possible sensitive data + t[0] = t[1] = c = 0; // clear size information + mz[] = 0; // clear input message buffer + // Only clear remaining of state if digest not at maximum. + // e.g., BLAKE2b-512 has a digest size of 64 bytes + // ulong[8] (8*8) is 64 bytes of state + // So is BLAKE2x-256 is used, a digest of 32 bytes + // state (h) will still be 64 bytes, so upper range is cleared + static if (MAXED == false) + h8[digestSizeBytes..$] = 0; // clear unused state space + + return h8[0..digestSizeBytes]; + } + +private: + + /*static if (key) + { + enum KEYLEN = key.length; + } + else + enum KEYLEN = 0;*/ + + public ubyte[32] key; + + enum digestSizeBytes = digestSize / 8; + // 3 2 1 0 + // p[0] = 0x0101kknn + // kk - Key size. Set to zero since HMAC is done elsewhere. + // nn - Digest size in bytes. + enum p0 = 0x0101_0000 ^ (0 << 8) ^ digestSizeBytes; + enum msz = 16 * inner_t.sizeof; /// message size in bytes + enum hsz = 8 * inner_t.sizeof; /// state size in bytes + + static assert(msz == BSIZE); // e.g. 128 for b2b and 64 for b2s + static assert(hsz == BSIZE / 2); // e.g., 64 for b2b and 32 for b2s + + union // input message buffer + { + size_t[BSIZE / size_t.sizeof] mz = void; + inner_t[16] m; /// Message + ubyte[16 * inner_t.sizeof] m8; /// Message in byte-size + } + union // state + { + struct // .init hack since start() can't cover this + { + inner_t h0 = IV[0] ^ p0; + inner_t h1 = IV[1]; + inner_t h2 = IV[2]; + inner_t h3 = IV[3]; + inner_t h4 = IV[4]; + inner_t h5 = IV[5]; + inner_t h6 = IV[6]; + inner_t h7 = IV[7]; + } + inner_t[8] h; /// State + ubyte[8 * inner_t.sizeof] h8; /// State in byte-size + } + inner_t[2] t; /// Total count of input size + size_t c; /// Counter, pointer for buffer + uint last; /// If set, this is the last block to compress. + + void compress() + { + //TODO: bswap m on BigEndian platforms? + + inner_t[16] v = void; + v[0] = h[0]; + v[1] = h[1]; + v[2] = h[2]; + v[3] = h[3]; + v[4] = h[4]; + v[5] = h[5]; + v[6] = h[6]; + v[7] = h[7]; + v[8] = IV[0]; + v[9] = IV[1]; + v[10] = IV[2]; + v[11] = IV[3]; + v[12] = t[0] ^ IV[4]; + v[13] = t[1] ^ IV[5]; + v[14] = last ? ~IV[6] : IV[6]; + v[15] = IV[7]; + + // See i=0 v[16] + + for (size_t round; round < ROUNDS; ++round) + { + // a b c d x y + G(v, 0, 4, 8, 12, m[SIGMA[round][ 0]], m[SIGMA[round][ 1]]); + G(v, 1, 5, 9, 13, m[SIGMA[round][ 2]], m[SIGMA[round][ 3]]); + G(v, 2, 6, 10, 14, m[SIGMA[round][ 4]], m[SIGMA[round][ 5]]); + G(v, 3, 7, 11, 15, m[SIGMA[round][ 6]], m[SIGMA[round][ 7]]); + G(v, 0, 5, 10, 15, m[SIGMA[round][ 8]], m[SIGMA[round][ 9]]); + G(v, 1, 6, 11, 12, m[SIGMA[round][10]], m[SIGMA[round][11]]); + G(v, 2, 7, 8, 13, m[SIGMA[round][12]], m[SIGMA[round][13]]); + G(v, 3, 4, 9, 14, m[SIGMA[round][14]], m[SIGMA[round][15]]); + + // See i=1..i=10/12 v[16] + } + + h[0] ^= v[0] ^ v[8]; + h[1] ^= v[1] ^ v[9]; + h[2] ^= v[2] ^ v[10]; + h[3] ^= v[3] ^ v[11]; + h[4] ^= v[4] ^ v[12]; + h[5] ^= v[5] ^ v[13]; + h[6] ^= v[6] ^ v[14]; + h[7] ^= v[7] ^ v[15]; + + // See h[8] + } + + static void G(ref inner_t[16] v, uint a, uint b, uint c, uint d, inner_t x, inner_t y) + { + v[a] = v[a] + v[b] + x; + v[d] = ror(v[d] ^ v[a], R1); + v[c] = v[c] + v[d]; + v[b] = ror(v[b] ^ v[c], R2); + v[a] = v[a] + v[b] + y; + v[d] = ror(v[d] ^ v[a], R3); + v[c] = v[c] + v[d]; + v[b] = ror(v[b] ^ v[c], R4); + } +} + +/// Alias for BLAKE2b-512 +public alias BLAKE2b512 = BLAKE2!(BLAKE2Variant.b, 512); +/// Alias for BLAKE2s-256 +public alias BLAKE2s256 = BLAKE2!(BLAKE2Variant.s, 256); + +/// Convience alias for $(REF digest, std,digest) using the BLAKE2b-512 implementation. +auto blake2b_Of(T...)(T data) { return digest!(BLAKE2b512, T)(data); } +/// Alias of blake2b_Of. +alias blake2_Of = blake2b_Of; +/// Convience alias for $(REF digest, std,digest) using the BLAKE2s-256 implementation. +auto blake2s_Of(T...)(T data) { return digest!(BLAKE2s256, T)(data); } + +/// OOP API BLAKE2 implementation aliases. +public alias BLAKE2b512Digest = WrapperDigest!BLAKE2b512; +/// Ditto +public alias BLAKE2s256Digest = WrapperDigest!BLAKE2s256; + +/// Of course they correspond to the digest API! +@safe unittest +{ + assert(isDigest!BLAKE2b512); + assert(isDigest!BLAKE2s256); +} + +/// Testing "Of" wrappers against digest wrappers. +@safe unittest +{ + enum TEXT = "abc"; + + ubyte[64] b2b = blake2b_Of(TEXT); + assert(b2b == digest!BLAKE2b512(TEXT)); + + ubyte[32] b2s = blake2s_Of(TEXT); + assert(b2s == digest!BLAKE2s256(TEXT)); +} + +/// Testing template API +@system unittest +{ + import std.conv : hexString; + + ubyte[] s = [ 'a', 'b', 'c' ]; + + BLAKE2s256 b2s; + b2s.put(s); + assert(b2s.finish() == cast(ubyte[])hexString!( + "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982")); +} + +/// Test against empty input +@safe unittest +{ + assert(toHexString!(LetterCase.lower)(blake2b_Of("")) == + "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419"~ + "d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"); + assert(toHexString!(LetterCase.lower)(blake2s_Of("")) == + "69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9"); +} + +/// Test against "abc" +@safe unittest +{ + assert(toHexString!(LetterCase.lower)(blake2b_Of("abc")) == + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"~ + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923"); + assert(toHexString!(LetterCase.lower)(blake2s_Of("abc")) == + "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982"); +} + + +/// Testing template API against one million 'a' +@system unittest +{ + import std.conv : hexString; + + ubyte[] onemilliona = new ubyte[1_000_000]; + onemilliona[] = 'a'; + + BLAKE2b512 b2b; + b2b.put(onemilliona); + assert(b2b.finish() == cast(ubyte[]) hexString!( + "98fb3efb7206fd19ebf69b6f312cf7b64e3b94dbe1a17107913975a793f177e1"~ + "d077609d7fba363cbba00d05f7aa4e4fa8715d6428104c0a75643b0ff3fd3eaf")); + + BLAKE2s256 b2s; + b2s.put(onemilliona); + assert(b2s.finish() == cast(ubyte[]) hexString!( + "bec0c0e6cde5b67acb73b81f79a67a4079ae1c60dac9d2661af18e9f8b50dfa5")); +} + +/// Testing OOP API +@system unittest +{ + import std.conv : hexString; + + ubyte[] s = ['a', 'b', 'c']; + + BLAKE2b512Digest b2b = new BLAKE2b512Digest(); + b2b.put(s); + assert(b2b.finish() == cast(ubyte[]) hexString!( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"~ + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923")); + + BLAKE2s256Digest b2s = new BLAKE2s256Digest(); + b2s.put(s); + assert(b2s.finish() == cast(ubyte[]) hexString!( + "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982")); +} + +/// BLAKE2s Template usage +@system unittest +{ + import std.conv : hexString; + + // Let's use the template features: + // NOTE: When passing a digest to a function, it must be passed by reference! + void doSomething(T)(ref T hash) + if (isDigest!T) + { + hash.put([ 'a', 'b', 'c' ]); + } + BLAKE2b512 b2b; + b2b.start(); + doSomething(b2b); + assert(b2b.finish() == cast(ubyte[]) hexString!( + "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d1"~ + "7d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923")); + BLAKE2s256 b2s; + b2s.start(); + doSomething(b2s); + assert(b2s.finish() == cast(ubyte[]) hexString!( + "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982")); +} + +/// Testing with HMAC +/*@system unittest +{ + import std.ascii : LetterCase; + import std.digest.hmac : hmac; + import std.string : representation; + + auto secret = "secret".representation; + + // Temporary hack + //alias HMACBLAKE2b512 = BLAKE2!(BLAKE2Variant.b, 512, "secret".representation); + //alias HMACBLAKE2s256 = BLAKE2!(BLAKE2Variant.s, 256, "secret".representation); + + assert("The quick brown fox jumps over the lazy dog" + .representation + .hmac!BLAKE2b512(secret) + .toHexString!(LetterCase.lower) == + "97504d0493aaaa40b08cf700fd380f17fe32e26e008fa20f9f3f04901d9f5bf3"~ + "3e826ea234f93bedfe7c5c50a540ad61454eb011581194cd68bff57938760ae0"); + assert("The quick brown fox jumps over the lazy dog" + .representation + .hmac!BLAKE2s256(secret) + .toHexString!(LetterCase.lower) == + "e95b806f87e9477966cd5f0ca2d496bfdfa424c69e820d33e4f1007aeb6c9de1"); +}*/ \ No newline at end of file