diff --git a/lib/std/hash/siphash.c3 b/lib/std/hash/siphash.c3 new file mode 100644 index 000000000..5795cac32 --- /dev/null +++ b/lib/std/hash/siphash.c3 @@ -0,0 +1,164 @@ +// Copyright (c) 2025 Zack Puhl . All rights reserved. +// Use of this source code is governed by the MIT license +// a copy of which can be found in the LICENSE_STDLIB file. +// +// SipHash is a secure pseudorandom function (PRF) which digests a 128-bit key +// and a variable-length message to produce a 64- or 128-bit hash value. +// +// SipHash can be employed in numerous useful ways and structures, e.g.: +// - Hash Tables +// - Message Authentication Codes +// - Denial of Service (hash flooding) resistance +// - Bloom filters +// - Keyed runtime identifier derivation +// +// Read more: https://en.wikipedia.org/wiki/SipHash +// +// + +// COMMON HASH VARIANTS. +// These two forms of SipHash (24 and 48) are the most widely +// used by many implementations. + +// These provide typical 64-bit hash results. +// -- Best for performance-critical applications. +module std::hash::siphash24; +import std::hash::siphash; +alias SipHash24 = SipHash { ulong, 2, 4 }; +alias hash = siphash::hash { ulong, 2, 4 }; + +// -- Best for conservative security applications. +module std::hash::siphash48; +import std::hash::siphash; +alias SipHash48 = SipHash { ulong, 4, 8 }; +alias hash = siphash::hash { ulong, 4, 8 }; + + +// Exact same as above, but for 128-bit outputs. Algorithm internally changes slightly. +module std::hash::siphash24_128; +import std::hash::siphash; +alias SipHash24_128 = SipHash { uint128, 2, 4 }; +alias hash = siphash::hash { uint128, 2, 4 }; + +module std::hash::siphash48_128; +import std::hash::siphash; +alias SipHash48_128 = SipHash { uint128, 4, 8 }; +alias hash = siphash::hash { uint128, 4, 8 }; + +<* +@require OutType.typeid == uint128.typeid || OutType.typeid == ulong.typeid : "Module OutType must be either uint128 or ulong." +*> +module std::hash::siphash { OutType, BLOCK_ROUNDS, FINALIZE_ROUNDS }; + + +struct SipHash +{ + ulong[4] v; + long m; + int m_idx; + usz len; +} + +fn OutType hash(char[] data, uint128 key) +{ + SipHash s; + s.init(key); + s.update(data); + return s.final(); +} + +fn void SipHash.init(&self, uint128 key) +{ + ulong[2] key_64 = bitcast(key, ulong[2]); + + self.v = { + 0x736f_6d65_7073_6575 ^ key_64[0], + 0x646f_7261_6e64_6f6d ^ key_64[1], + 0x6c79_6765_6e65_7261 ^ key_64[0], + 0x7465_6462_7974_6573 ^ key_64[1], + }; + + $if OutType.typeid == uint128.typeid : + self.v[1] ^= 0xEE; + $endif +} + +<* + @param [in] data : "Bytes to hash" +*> +fn void SipHash.update(&self, char[] data) +{ + self.len += data.len; + + foreach (byte : data) + { + self.m |= (long)byte << (self.m_idx++ << 3); + + if (self.m_idx < 8) continue; + + // This runs every time the m_idx is 8, then it resets to 0. + self.v[3] ^= self.m; + + $for var $i = 0; $i < BLOCK_ROUNDS; ++$i : // unrolled loop + self.round(); + $endfor + + self.v[0] ^= self.m; + + self.m_idx = 0; + self.m = 0; + } +} + +fn OutType SipHash.final(&self) +{ + char[8] last = { [7] = (char)self.len }; + + self.update(last[(self.m_idx < 7 ? self.m_idx : 7)..]); + + $if OutType.typeid == uint128.typeid : + self.v[2] ^= 0xEE; + $else + self.v[2] ^= 0xFF; + $endif + + $for var $i = 0; $i < FINALIZE_ROUNDS; ++$i : // unrolled loop + self.round(); + $endfor + + $if OutType.typeid == ulong.typeid : + return self.v[0] ^ self.v[1] ^ self.v[2] ^ self.v[3]; + $else + ulong lo = self.v[0] ^ self.v[1] ^ self.v[2] ^ self.v[3]; + + self.v[1] ^= 0xDD; + + $for var $i = 0; $i < FINALIZE_ROUNDS; ++$i : // unrolled loop + self.round(); + $endfor + + return lo | ((uint128)(self.v[0] ^ self.v[1] ^ self.v[2] ^ self.v[3]) << 64); + $endif +} + + +fn void SipHash.round(&self) @local +{ + self.v[0] += self.v[1]; + self.v[1] = self.v[1].rotl(13); + self.v[1] ^= self.v[0]; + self.v[0] = self.v[0].rotl(32); + self.v[2] += self.v[3]; + self.v[3] = self.v[3].rotl(16); + self.v[3] ^= self.v[2]; + self.v[0] += self.v[3]; + self.v[3] = self.v[3].rotl(21); + self.v[3] ^= self.v[0]; + self.v[2] += self.v[1]; + self.v[1] = self.v[1].rotl(17); + self.v[1] ^= self.v[2]; + self.v[2] = self.v[2].rotl(32); +} + + + diff --git a/releasenotes.md b/releasenotes.md index ddd4cdda0..1aa9f1a6a 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -15,6 +15,7 @@ - Formatting option "%h" now supports pointers. - Improve error on unsigned implicit conversion to signed. - Update error message for struct initialization #2286 +- Add SipHash family of keyed PRFs. #2287 - `$is_const` is deprecated in favour of `@is_const` based on `$defined`. - Multiline contract comments #2113 - Removed the use of temp allocator in backtrace printing. diff --git a/test/unit/stdlib/hash/siphash.c3 b/test/unit/stdlib/hash/siphash.c3 new file mode 100644 index 000000000..23b1c9855 --- /dev/null +++ b/test/unit/stdlib/hash/siphash.c3 @@ -0,0 +1,215 @@ +// Copyright (c) 2025 Zack Puhl . All rights reserved. +// Use of this source code is governed by the MIT license +// a copy of which can be found in the LICENSE_STDLIB file. +module std::hash::siphash_tests; +import std::math; + +char[64] plaintext = math::iota(char[64]); + + +module std::hash::siphash_tests @test; + +import std::hash; + +const uint128 DEFAULT_TEST_KEY = 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100; + +fn void sanity_check_default_test_key() + => test::@check(((char*)&DEFAULT_TEST_KEY)[:16] == { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, "Bad key value as char[]."); + + +fn void streamed_plaintext_64() +{ + ulong expected = 0x958a324ceb064572; + + SipHash24 s; + + s.init(DEFAULT_TEST_KEY); + + s.update(plaintext[:5]); + s.update(plaintext[5:25]); + s.update(plaintext[30..^2]); // because the end of the for-loop doesn't get to the full 64 chars, it's 63 + + ulong hashval = s.final(); + + test::@check(hashval == expected, "Expected streamed hash mismatch [%x expected // %x actual].", expected, hashval); +} + + +fn void streamed_plaintext_128() +{ + uint128 expected = 0x7cbd3f979a063e504a83502f77d15051; + + SipHash24_128 s; + + s.init(DEFAULT_TEST_KEY); + + s.update(plaintext[:1]); + s.update(plaintext[1:2]); + s.update(plaintext[3:12]); + s.update(plaintext[15..^2]); // because the end of the for-loop doesn't get to the full 64 chars, it's 63 + + uint128 hashval = s.final(); + + test::@check(hashval == expected, "Expected streamed hash mismatch [%x expected // %x actual].", expected, hashval); +} + + +// These siphash-2,4 vectors are from the great and wise sages themselves: +// https://github.com/veorq/SipHash/blob/master/vectors.h +const ulong[64] VECTORS_24 = { + 0x726fdb47dd0e0e31, 0x74f839c593dc67fd, 0x0d6c8009d9a94f5a, 0x85676696d7fb7e2d, + 0xcf2794e0277187b7, 0x18765564cd99a68d, 0xcbc9466e58fee3ce, 0xab0200f58b01d137, + 0x93f5f5799a932462, 0x9e0082df0ba9e4b0, 0x7a5dbbc594ddb9f3, 0xf4b32f46226bada7, + 0x751e8fbc860ee5fb, 0x14ea5627c0843d90, 0xf723ca908e7af2ee, 0xa129ca6149be45e5, + 0x3f2acc7f57c29bdb, 0x699ae9f52cbe4794, 0x4bc1b3f0968dd39c, 0xbb6dc91da77961bd, + 0xbed65cf21aa2ee98, 0xd0f2cbb02e3b67c7, 0x93536795e3a33e88, 0xa80c038ccd5ccec8, + 0xb8ad50c6f649af94, 0xbce192de8a85b8ea, 0x17d835b85bbb15f3, 0x2f2e6163076bcfad, + 0xde4daaaca71dc9a5, 0xa6a2506687956571, 0xad87a3535c49ef28, 0x32d892fad841c342, + 0x7127512f72f27cce, 0xa7f32346f95978e3, 0x12e0b01abb051238, 0x15e034d40fa197ae, + 0x314dffbe0815a3b4, 0x027990f029623981, 0xcadcd4e59ef40c4d, 0x9abfd8766a33735c, + 0x0e3ea96b5304a7d0, 0xad0c42d6fc585992, 0x187306c89bc215a9, 0xd4a60abcf3792b95, + 0xf935451de4f21df2, 0xa9538f0419755787, 0xdb9acddff56ca510, 0xd06c98cd5c0975eb, + 0xe612a3cb9ecba951, 0xc766e62cfcadaf96, 0xee64435a9752fe72, 0xa192d576b245165a, + 0x0a8787bf8ecb74b2, 0x81b3e73d20b49b6f, 0x7fa8220ba3b2ecea, 0x245731c13ca42499, + 0xb78dbfaf3a8d83bd, 0xea1ad565322a1a0b, 0x60e61c23a3795013, 0x6606d7e446282b93, + 0x6ca4ecb15c5f91e1, 0x9f626da15c9625f3, 0xe51b38608ef25f57, 0x958a324ceb064572, +}; + +fn void siphash24_vectors() +{ + for (usz i = 0; i < VECTORS_24.len; ++i) + { + ulong hashval = siphash24::hash(plaintext[:i], DEFAULT_TEST_KEY); + + test::@check(hashval == VECTORS_24[i], "Expected hash mismatch [%x expected // %x actual].", VECTORS_24[i], hashval); + } +} + + +const ulong[64] VECTORS_48 = { + 0xc879052b9938da41, 0xc85914f95295b851, 0x33c3ddbef0163792, 0x05c147657dd4466a, + 0x48fac14a2b5938c2, 0xe14752cfd9d7c2f6, 0x8e5535c834bcb66b, 0x4efdbe5a713fd747, + 0x50db2f079c8bb520, 0x5312e15ef39a3136, 0x8f848d0adbd0a948, 0x810a0436603969cc, + 0x6197a77a53686d4b, 0x6950c9f2e9963729, 0x689a62a7ea1b4388, 0x83d389d57da9a6e0, + 0x70acb28053f59c55, 0x4e79e37a11c5b7d5, 0x2b10ad3446453c5a, 0xbc3d5aa3af80a4c0, + 0xc84b28e50927c278, 0x9dd6eb0d467026ef, 0xd884d0a986ef76d9, 0xe8d0ea191881d9e3, + 0x16ecea3eb53c3389, 0xc64973645f6c1531, 0xa432763535ce4ca5, 0xfed2a7c025895d06, + 0x8b3a1a2282aabb2b, 0x707b0964cefb0b87, 0x8bee9564f9e0d840, 0x12dffa0bf4a7fc79, + 0xd29e762ff2fb0b00, 0xfa22e5f891556840, 0x0d9d14d874fee62b, 0xed60750b0e2f7eba, + 0x97e1a7ed84e3e902, 0xb6632795620ae8c4, 0xd36d5c5dc6ed2783, 0xc02fa464d164fc79, + 0x4e61fccb11754a15, 0x6fe6a0ec7c8d148b, 0xfa03c454b669eedf, 0xc9b77b69a6368fc5, + 0x2131c6059cbec5a6, 0x3189cdfb59878ab5, 0x25c4cc04673a68d7, 0x8d44a2e5e1e66acb, + 0x73513a3a5b69266e, 0x4aac339fcf077178, 0x84747bd9da907516, 0x06f36bf01e686b00, + 0xa6cfef6602309b1c, 0x4bb3b0d1882f8d28, 0xfe6bf5acbd0611e0, 0x28036e5b0e1f10c0, + 0x0a1c1b5b4591a7c3, 0x0f3a0b9ee1af0757, 0x4f5953fe29725ae6, 0x4caf1aabb99d2f00, + 0x0606c14450cb2859, 0x2173857b960138d5, 0xcc99091a4f36db05, 0x23de0355bc8477e6, +}; + +fn void siphash48_vectors() +{ + for (usz i = 0; i < VECTORS_48.len; ++i) + { + ulong hashval = siphash48::hash(plaintext[:i], DEFAULT_TEST_KEY); + + test::@check(hashval == VECTORS_48[i], "Expected hash mismatch [%x expected // %x actual].", VECTORS_48[i], hashval); + } +} + + +// Yet again, these test vectors are prized heirloom values handed down through the generations: +// https://github.com/veorq/SipHash/blob/master/vectors.h +const uint128[64] VECTORS_24_128 = { + 0x930255c71472f66de6a825ba047f81a3, 0x45fc229b1159763444af996bd8c187da, 0xe4ff0af6de8ba3fcc75da4a48d227781, 0x51ed8529b0b6335f4ea967520cb6709c, + 0x7955cd7b7c6e0f7daf8f9c2dc16481f8, 0x27960e69077a5254886f778059876813, 0x5ea1d78f30a05e481386208b33caee14, 0x3982f01fa64ab8c053c1dbd8beebf1a1, + 0xb49714f364e2830f61f55862baa9623b, 0xed716dbb028b7fc4abbad90a06994426, 0xbafbd0f3d34754c956691478c30d1100, 0x18dce5816fdcb4a277666b3868c55101, + 0x25c13285f64d638258f35e9066b226d6, 0xf752b9c44f9329d0108bc0e947e26998, 0x024949e45f48c77e9cded766aceffc31, 0xd9c3cf970fec087e11a8b03399e99354, + 0x77052385bf1533fdbb54b067caa4e26e, 0x4077e47ac466c05498b88d73e8063d47, 0x23f7aefe81a44d298548bf23e4e526a4, 0xb12e51528920d574b0fa65cf31770178, + 0xeb3938e8a544933e7390223f83fc259e, 0x121d073ecd14228a215a52be5a498e56, 0xae0aff8e52109c469a6bd15245b5294a, 0x1c69bf9a9ae28ccfe0f5a9d5dd84d1c9, + 0xad32618a178a2a88d850bd78ae79b42d, 0x6f8f8dcbeab951507b445e2d045fce8e, 0x661f147886e0ae7ee807c3b3b4530b9c, 0x94eb9e122febd3bfe4eaa669af48f2ab, + 0xf4ae587302f335b9884b576816da6406, 0xb76a7c463cfdd40ce97d33bfc49d4baa, 0x87226d68d4d71a2bde6baf1f477f5cea, 0x353dc4524fde2317fcfa233218b03929, + 0x68eb4665559d3e363efcea5eca56397c, 0xcfffa94e5f9db6b6321cf0467107c677, 0xde549b30f1f02509df7e84b86c98a637, 0xc88c3c922e1a2407f9a8a99de6f005a7, + 0x11674f90ed769e1e4648c4291f7dc43d, 0x2b69d3c551473c0d1a0efce601bf620d, 0xb5e7be4b085efde49e667cca8b46038c, 0xd92bd2d0e5cc73449c2caf3bb95b8a52, + 0xd83b91c6c80cae97ad5dc9951e306adf, 0xdbb6705e289135e7397f852c90891180, 0x5b0ccacc34ae5036bb31c2c96a3417e6, 0x89df5aecdc211840aa21b7ef3734d927, + 0x4273cc66b1c9b1d8785e9ced9d7d2389, 0x4cb150a294fa8911657d5ebf91806d4a, 0x022949cf3d0efc3f89aee75560f9330e, 0x1b1563dc4bd8c88ed1190b722b431ce6, + 0x169b2608a6559037cf82f749f5aee5f7, 0x03641a20adf237a84fa5b7d00f038d43, 0x3f4286f2270d7e24e304bf4feed390a5, 0x38f5f9ae7cd35cb1c493fe72a1c1e25f, + 0x7c013a8bd03d13b26eb306bd5c32972c, 0x9ed32a009f65f09f94ca6b7a2214c892, 0x871d91d64108d5fb8c32d80b1150e8dc, 0xda832592b52be3481279dac78449f167, + 0x362a1da96f16947ee94ed572cff23819, 0x8e6904163024620ffe49ed46961e4874, 0x1d8a3d58d0386400d8d6a998dea5fc57, 0x595357d9743676d4be1cdcef1cdeec9f, + 0x40e772d8cb73ca6653f128eb000c04e3, 0x7a0f6793591ca9ccfe1d836a9a009776, 0xbd5947f0a447d505a067f52123545358, 0x7cbd3f979a063e504a83502f77d15051, +}; + +fn void siphash24_128_vectors() +{ + for (usz i = 0; i < VECTORS_24_128.len; ++i) + { + uint128 hashval = siphash24_128::hash(plaintext[:i], DEFAULT_TEST_KEY); + + test::@check(hashval == VECTORS_24_128[i], "Expected hash mismatch [%x expected // %x actual].", VECTORS_24_128[i], hashval); + } +} + + +const uint128[64] VECTORS_48_128 = { + 0x6c0aa78354e8eccfe904a96d58ce641f, 0x80a2c791a77cf26a47794cefa85d3447, 0xa42182185f817322c62dca96a35f49e1, 0x027605817fb69c5a834ec54a8473a2c7, + 0xef6d651fe0c8952a4ece3ef4bb521f54, 0x5dbe12bf0c994f241548f30dd43b9717, 0xfff5e108c9567db1cd8032560d360b6b, 0xe0eed2ff548b6b72c2f14b183be100ed, + 0x63e0caaf235a4a36f5edf98f1346d9a7, 0xe80bfbe449559a8ba3ec5c54b714739e, 0x7512b99a885be6ae68d18984c6626c58, 0x997a2e2f1e84abc447d1a34ca65271e6, + 0xaf16a8ee83089e3e2ee58d90ea7a1c7f, 0x344aefb50d33353f9233b792bf7a82de, 0xf97d4c35e28e6737c59a370f64637559, 0xcb6290dcfaf7783d593a453a30034d28, + 0x9cad4e8dabe5fdc0b7637f59a2c74a91, 0xcbc387ce953b453aee55a44ba415510d, 0x1479ce845a88378394d8f10b9d939b54, 0x6cb4728c5713fbd2eb8a34cd6997176c, + 0x8b40862fee002a68e057c938c136d0aa, 0x263ad74eff4490eebf70b62fc4eeb121, 0xeb143f4617c9534637ed9729d6a19305, 0x4ee25786c6fff1a01ef9197762313d11, + 0xd6c452c2f014730edd6ae02df74c39b3, 0xd28963cde46b45e241c3359dda982a92, 0x4d8db2cb50eadca24ab357f730626b59, 0x48ee596575df8406807e5bd597e44ec2, + 0x778101dce49d2b5ed41e6836a1b69c5e, 0xb278819dfeed337904d35686ca39fabf, 0xa3ae34813a807a355a79d0a118942218, 0x6eb0771a57697b732e4e4753ff963e4a, + 0x5094f1632f572f2e847237d0f9f0d5fe, 0x6f44bcc629660cc46342f9c186583339, 0x9fbf8b1997462c8bb01087b33bf9a5ee, 0x3ad46661061243370e724f70b6c76e80, + 0xa2b24760aeeeaf9db439c9f09ded696e, 0x38d72f7f1730a294c7f9b698f27bc793, 0xe60ce64c2a3affda75a82a8cd99cadff, 0xe310f55776ef64cdcd934af9fd2f994d, + 0x9bce1bbe96e086a11ea1e0244e627032, 0x77313409c4c7ff1f51ff4ecbe0bbe831, 0x5bab1670a0128a6407d99a87057de1cb, 0xfc0a6a36468bd2d5e28be97043d44888, + 0x0c5e095465cfb50ca9761042b2d1ffb7, 0x0c05d11ffe84d8bbf6a823d56c666b6a, 0x24621330bd8cf105c8f5fb50838afea8, 0xb5627ed594963aebf23682ee7a11e7cc, + 0x049ff6feba90306b0cb728fce4f0253a, 0x4add3144e4f806818bc49f7426e6053f, 0x5b3477dcfad5ff0a5c167276f9796876, 0x0e2fdda19484c2c98b596cb65aa07143, + 0x6602e0d6efb6221fbaf1a5a2d35bf865, 0x1166af8a229f6acaef224be5da61cf76, 0xda84a49dab9053889fa2db9fe3c2dc6c, 0x5aaa2f7b61e2e4d8b2673bcceaaceee1, + 0x6d5aabd52eb0d678170fe14c6f9fd20b, 0x07a6655b834587e07c26526a159f18ad, 0x8f211ce3996bec3dd466257271996b0f, 0x741a9e3d6f9cf3d366f43d4ffac8a4a1, + 0x1df99e2da7d8a6c11fc2f08cb83d1a3b, 0xe52e698a5a63c3562800c0ef026848d1, 0x3c1c2cc2314956fd9919ae7c8f5fa1ee, 0xa5d46b90ee61792093dbc42863aef563, +}; + +fn void siphash48_128_vectors() +{ + for (usz i = 0; i < VECTORS_48_128.len; ++i) + { + uint128 hashval = siphash48_128::hash(plaintext[:i], DEFAULT_TEST_KEY); + + test::@check(hashval == VECTORS_48_128[i], "Expected hash mismatch [%x expected // %x actual].", VECTORS_48_128[i], hashval); + } +} + + +alias SecHash = SipHash { uint128, 256, 512 }; +alias sec_hash = siphash::hash { uint128, 256, 512 }; + +fn void test_custom_sip() +{ + uint128 key = 0x8a52f8467c2fafa9ab4d7396d6fe01a3; + char[] data = + "As you can see, the hash rounds are flexible and so is the return type, between a 64-bit and 128-bit key." + " Output values are simple, quick, and deterministic." + .tcopy(); + + uint128 expected = 0x47de68b601e964b343c29358f864cc40; + uint128 actual = sec_hash(data, key); + + test::@check(actual == expected, "Expected hash mismatch [%x expected // %x actual].", expected, actual); + + // ---------- Slight data change. + data[0] = 'a'; + expected = 0x15fa9037e63089732d86ab5bab3938e1; + actual = sec_hash(data, key); + + test::@check(actual == expected, "Expected hash mismatch [%x expected // %x actual].", expected, actual); + + // ---------- Slight key change; streamed. + key -= 1; + expected = 0xceeaca813e87ad83dde110a402eac1ef; + SecHash s; + s.init(key); + s.update(data[:2]); + s.update(data[2..^2]); + s.update(data[^1..]); + actual = s.final(); + + test::@check(actual == expected, "Expected hash mismatch [%x expected // %x actual].", expected, actual); +}