diff --git a/Tests/LibCrypto/CMakeLists.txt b/Tests/LibCrypto/CMakeLists.txt index e17909529cb..b9bf2c998c5 100644 --- a/Tests/LibCrypto/CMakeLists.txt +++ b/Tests/LibCrypto/CMakeLists.txt @@ -3,13 +3,14 @@ set(TEST_SOURCES TestASN1.cpp TestBigFraction.cpp TestBigInteger.cpp - TestChecksum.cpp TestChaCha20.cpp TestChacha20Poly1305.cpp + TestChecksum.cpp TestCurves.cpp TestEd25519.cpp TestHash.cpp TestHMAC.cpp + TestMGF.cpp TestPBKDF2.cpp TestPoly1305.cpp TestRSA.cpp diff --git a/Tests/LibCrypto/TestMGF.cpp b/Tests/LibCrypto/TestMGF.cpp new file mode 100644 index 00000000000..d6a834936aa --- /dev/null +++ b/Tests/LibCrypto/TestMGF.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +static ByteBuffer operator""_b(char const* string, size_t length) +{ + return ByteBuffer::copy(string, length).release_value(); +} + +TEST_CASE(test_mgf1_short) +{ + u8 expected_result[3] { + 0x1a, 0xc9, 0x07 + }; + auto expected = ReadonlyBytes { expected_result, 3 }; + + ByteBuffer seed = { "foo"_b }; + auto length = 3; + ByteBuffer result = MUST(Crypto::Hash::MGF::mgf1(seed, length)); + + EXPECT_EQ(expected, result); +} + +TEST_CASE(test_mgf1_long) +{ + u8 expected_result[50] { + 0xbc, 0x0c, 0x65, 0x5e, 0x01, 0x6b, 0xc2, 0x93, 0x1d, 0x85, 0xa2, 0xe6, 0x75, 0x18, 0x1a, 0xdc, + 0xef, 0x7f, 0x58, 0x1f, 0x76, 0xdf, 0x27, 0x39, 0xda, 0x74, 0xfa, 0xac, 0x41, 0x62, 0x7b, 0xe2, + 0xf7, 0xf4, 0x15, 0xc8, 0x9e, 0x98, 0x3f, 0xd0, 0xce, 0x80, 0xce, 0xd9, 0x87, 0x86, 0x41, 0xcb, + 0x48, 0x76 + }; + auto expected = ReadonlyBytes { expected_result, 50 }; + + ByteBuffer seed = { "bar"_b }; + auto length = 50; + ByteBuffer result = MUST(Crypto::Hash::MGF::mgf1(seed, length)); + + EXPECT_EQ(expected, result); +} + +TEST_CASE(test_mgf1_long_sha256) +{ + u8 expected_result[50] { + 0x38, 0x25, 0x76, 0xa7, 0x84, 0x10, 0x21, 0xcc, 0x28, 0xfc, 0x4c, 0x09, 0x48, 0x75, 0x3f, 0xb8, + 0x31, 0x20, 0x90, 0xce, 0xa9, 0x42, 0xea, 0x4c, 0x4e, 0x73, 0x5d, 0x10, 0xdc, 0x72, 0x4b, 0x15, + 0x5f, 0x9f, 0x60, 0x69, 0xf2, 0x89, 0xd6, 0x1d, 0xac, 0xa0, 0xcb, 0x81, 0x45, 0x02, 0xef, 0x04, + 0xea, 0xe1 + }; + auto expected = ReadonlyBytes { expected_result, 50 }; + + ByteBuffer seed = { "bar"_b }; + auto length = 50; + ByteBuffer result = MUST(Crypto::Hash::MGF::mgf1(seed, length)); + + EXPECT_EQ(expected, result); +} diff --git a/Userland/Libraries/LibCrypto/Hash/MGF.h b/Userland/Libraries/LibCrypto/Hash/MGF.h new file mode 100644 index 00000000000..423591dc479 --- /dev/null +++ b/Userland/Libraries/LibCrypto/Hash/MGF.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Crypto::Hash { + +class MGF { +public: + // https://datatracker.ietf.org/doc/html/rfc2437#section-10.2.1 + template + static ErrorOr mgf1(ReadonlyBytes seed, size_t length) + requires requires { HashFunction::digest_size(); } + { + HashFunction hash; + + size_t h_len = hash.digest_size(); + + // 1. If length > 2^32(hLen), output "mask too long" and stop. + if (length > (h_len << 32)) + return Error::from_string_view("mask too long"sv); + + // 2. Let T be the empty octet string. + auto t = TRY(ByteBuffer::create_uninitialized(0)); + + // 3. For counter from 0 to ceil(length / hLen) - 1, do the following: + auto counter = 0u; + auto iterations = AK::ceil_div(length, h_len) - 1; + + auto c = TRY(ByteBuffer::create_uninitialized(4)); + for (; counter <= iterations; ++counter) { + // a. Convert counter to an octet string C of length 4 with the primitive I2OSP: C = I2OSP(counter, 4) + ByteReader::store(static_cast(c.data()), AK::convert_between_host_and_big_endian(static_cast(counter))); + + // b. Concatenate the hash of the seed Z and C to the octet string T: T = T || Hash (Z || C) + hash.update(seed); + hash.update(c); + auto digest = hash.digest(); + + TRY(t.try_append(digest.bytes())); + } + + // 4. Output the leading l octets of T as the octet string mask. + return t.slice(0, length); + } +}; + +}