From 03c126e384813c0cfb70ae985217e9f84b3df71c Mon Sep 17 00:00:00 2001 From: Tete17 Date: Thu, 27 Nov 2025 14:36:59 +0100 Subject: [PATCH] LibCrypto: Implement Argon2 hash algorithm --- Libraries/LibCrypto/CMakeLists.txt | 1 + Libraries/LibCrypto/Hash/Argon2.cpp | 92 ++++++++++++++++ Libraries/LibCrypto/Hash/Argon2.h | 43 ++++++++ Tests/LibCrypto/CMakeLists.txt | 1 + Tests/LibCrypto/TestArgon2.cpp | 156 ++++++++++++++++++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 Libraries/LibCrypto/Hash/Argon2.cpp create mode 100644 Libraries/LibCrypto/Hash/Argon2.h create mode 100644 Tests/LibCrypto/TestArgon2.cpp diff --git a/Libraries/LibCrypto/CMakeLists.txt b/Libraries/LibCrypto/CMakeLists.txt index 9b8845af76..b4415216e5 100644 --- a/Libraries/LibCrypto/CMakeLists.txt +++ b/Libraries/LibCrypto/CMakeLists.txt @@ -13,6 +13,7 @@ set(SOURCES Cipher/AES.cpp Curves/EdwardsCurve.cpp Curves/SECPxxxr1.cpp + Hash/Argon2.cpp Hash/BLAKE2b.cpp Hash/HKDF.cpp Hash/MD5.cpp diff --git a/Libraries/LibCrypto/Hash/Argon2.cpp b/Libraries/LibCrypto/Hash/Argon2.cpp new file mode 100644 index 0000000000..6a04bfb76f --- /dev/null +++ b/Libraries/LibCrypto/Hash/Argon2.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, Miguel Sacristán Izcue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include + +#include +#include +#include +#include + +namespace Crypto::Hash { + +static char const* argon2_type_to_openssl_name(Argon2Type type) +{ + switch (type) { + case Argon2Type::Argon2d: + return "ARGON2d"; + case Argon2Type::Argon2i: + return "ARGON2i"; + case Argon2Type::Argon2id: + return "ARGON2id"; + default: + VERIFY_NOT_REACHED(); + } +} + +Argon2::Argon2(Argon2Type type) + : m_kdf(EVP_KDF_fetch(nullptr, argon2_type_to_openssl_name(type), nullptr)) +{ +} + +Argon2::~Argon2() +{ + EVP_KDF_free(m_kdf); +} + +ErrorOr Argon2::derive_key( + ReadonlyBytes message, + ReadonlyBytes nonce, + u32 parallelism, + u32 memory, + u32 passes, + u32 version, + Optional secret_value, + Optional associated_data, + u32 tag_length) const +{ + auto ctx = TRY(OpenSSL_KDF_CTX::wrap(EVP_KDF_CTX_new(m_kdf))); + + auto threads = min(OSSL_get_max_threads(nullptr), parallelism); + + OSSL_PARAM params[] = { + OSSL_PARAM_uint32(OSSL_KDF_PARAM_ARGON2_LANES, ¶llelism), + OSSL_PARAM_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memory), + OSSL_PARAM_octet_string(OSSL_KDF_PARAM_SALT, const_cast(nonce.data()), nonce.size()), + OSSL_PARAM_octet_string(OSSL_KDF_PARAM_PASSWORD, const_cast(message.data()), message.size()), + OSSL_PARAM_uint32(OSSL_KDF_PARAM_ARGON2_VERSION, &version), + OSSL_PARAM_uint32(OSSL_KDF_PARAM_ITER, &passes), + OSSL_PARAM_uint32(OSSL_KDF_PARAM_SIZE, &tag_length), + OSSL_PARAM_END, + OSSL_PARAM_END, + OSSL_PARAM_END, + OSSL_PARAM_END, + }; + + auto insertion_point = 7; + + if (threads != 0) { + params[insertion_point++] = OSSL_PARAM_uint32(OSSL_KDF_PARAM_THREADS, &threads); + } + + if (secret_value.has_value()) { + params[insertion_point++] = OSSL_PARAM_octet_string(OSSL_KDF_PARAM_SECRET, const_cast(secret_value->data()), secret_value->size()); + } + + if (associated_data.has_value()) { + params[insertion_point++] = OSSL_PARAM_octet_string(OSSL_KDF_PARAM_ARGON2_AD, const_cast(associated_data->data()), associated_data->size()); + } + + auto buf = TRY(ByteBuffer::create_uninitialized(tag_length)); + OPENSSL_TRY(EVP_KDF_derive(ctx.ptr(), buf.data(), tag_length, params)); + + return buf; +} + +} diff --git a/Libraries/LibCrypto/Hash/Argon2.h b/Libraries/LibCrypto/Hash/Argon2.h new file mode 100644 index 0000000000..bf88f512c6 --- /dev/null +++ b/Libraries/LibCrypto/Hash/Argon2.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, Miguel Sacristán Izcue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Crypto::Hash { + +enum class Argon2Type { + Argon2d, + Argon2i, + Argon2id +}; + +class Argon2 { + AK_MAKE_NONCOPYABLE(Argon2); + +public: + explicit Argon2(Argon2Type); + + ~Argon2(); + + ErrorOr derive_key( + ReadonlyBytes message, + ReadonlyBytes nonce, + u32 parallelism, + u32 memory, + u32 passes, + u32 version, + Optional secret_value, + Optional associated_data, + u32 tag_length) const; + +private: + EVP_KDF* m_kdf; +}; + +} diff --git a/Tests/LibCrypto/CMakeLists.txt b/Tests/LibCrypto/CMakeLists.txt index b98afedc83..486ce9cf45 100644 --- a/Tests/LibCrypto/CMakeLists.txt +++ b/Tests/LibCrypto/CMakeLists.txt @@ -1,5 +1,6 @@ set(TEST_SOURCES TestAES.cpp + TestArgon2.cpp TestASN1.cpp TestBigFraction.cpp TestBigInteger.cpp diff --git a/Tests/LibCrypto/TestArgon2.cpp b/Tests/LibCrypto/TestArgon2.cpp new file mode 100644 index 0000000000..9eb8b35138 --- /dev/null +++ b/Tests/LibCrypto/TestArgon2.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025, Miguel Sacristán Izcue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +// https://www.rfc-editor.org/rfc/rfc9106 +TEST_CASE(ARGON2d) +{ + Crypto::Hash::Argon2 argon2(Crypto::Hash::Argon2Type::Argon2d); + + u8 const message[32] = { + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 + }; + + u8 const nonce[16] = { + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02 + }; + + u8 const secret_value[8] = { + 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03 + }; + + u8 const associated_data[12] = { + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 + }; + + auto const result = TRY_OR_FAIL(argon2.derive_key( + message, + nonce, + 4, + 32, + 3, + 0x13, + TRY_OR_FAIL(ByteBuffer::copy(secret_value, sizeof(secret_value))).span(), + TRY_OR_FAIL(ByteBuffer::copy(associated_data, sizeof(associated_data))).span(), + 256 / 8)); + + u8 const expected_output_key[] = { + 0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97, + 0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94, + 0xf8, 0x68, 0xe3, 0xbe, 0x39, 0x84, 0xf3, 0xc1, + 0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb + }; + + EXPECT_EQ(result.bytes(), ReadonlyBytes(expected_output_key)); +} + +TEST_CASE(ARGON2i) +{ + Crypto::Hash::Argon2 argon2(Crypto::Hash::Argon2Type::Argon2i); + + u8 const message[32] = { + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 + }; + + u8 const nonce[16] = { + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02 + }; + + u8 const secret_value[8] = { + 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03 + }; + + u8 const associated_data[12] = { + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 + }; + + auto const result = TRY_OR_FAIL(argon2.derive_key( + message, + nonce, + 4, + 32, + 3, + 0x13, + TRY_OR_FAIL(ByteBuffer::copy(secret_value, sizeof(secret_value))).span(), + TRY_OR_FAIL(ByteBuffer::copy(associated_data, sizeof(associated_data))).span(), + 256 / 8)); + + u8 const expected_output_key[] = { + 0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa, + 0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1, + 0xc8, 0xde, 0x6b, 0x01, 0x6d, 0xd3, 0x88, 0xd2, + 0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8 + }; + + EXPECT_EQ(result.bytes(), ReadonlyBytes(expected_output_key)); +} + +TEST_CASE(ARGON2id) +{ + Crypto::Hash::Argon2 argon2(Crypto::Hash::Argon2Type::Argon2id); + + u8 const message[32] = { + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 + }; + + u8 const nonce[16] = { + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02 + }; + + u8 const secret_value[8] = { + 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03 + }; + + u8 const associated_data[12] = { + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 + }; + + auto const result = TRY_OR_FAIL(argon2.derive_key( + message, + nonce, + 4, + 32, + 3, + 0x13, + TRY_OR_FAIL(ByteBuffer::copy(secret_value, sizeof(secret_value))).span(), + TRY_OR_FAIL(ByteBuffer::copy(associated_data, sizeof(associated_data))).span(), + 256 / 8)); + + u8 const expected_output_key[] = { + 0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, + 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, + 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, + 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59 + }; + + EXPECT_EQ(result.bytes(), ReadonlyBytes(expected_output_key)); +}