diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..8df68ff02 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev libglew2.2 \ libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev \ - libcurl4-openssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind + libcurl4-openssl-dev libssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind # fix luajit paths sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua @@ -46,7 +46,7 @@ jobs: - name: Build run: cmake --build build -t install - name: Run tests - run: ctest --test-dir build + run: ctest --output-on-failure --test-dir build - name: Run engine tests timeout-minutes: 1 run: | diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index f41e940e9..52f7b7183 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -39,7 +39,7 @@ jobs: # make && make install INSTALL_INC=/usr/include/lua run: | sudo apt-get update - sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libgtest-dev libcurl4-openssl-dev libfreetype6-dev + sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libgtest-dev libcurl4-openssl-dev libssl-dev libfreetype6-dev # fix luajit paths sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua-5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua diff --git a/Dockerfile b/Dockerfile index da90324fc..3a06d1afc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libvorbis-dev \ libcurl4-openssl-dev \ libfreetype6-dev \ + libssl-dev \ ca-certificates \ wget \ && rm -rf /var/lib/apt/lists/* diff --git a/doc/en/scripting.md b/doc/en/scripting.md index 2072d4353..5557c8185 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -14,7 +14,8 @@ Subsections: - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) - - [byteutil](scripting/builtins/libbyteutil.md) + - [byteutil](scripting/builtins/libbyteutil.md) + - [crypto](scripting/builtins/libcrypto.md) - [cameras](scripting/builtins/libcameras.md) - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) diff --git a/doc/en/scripting/builtins/libcrypto.md b/doc/en/scripting/builtins/libcrypto.md new file mode 100644 index 000000000..19b93e433 --- /dev/null +++ b/doc/en/scripting/builtins/libcrypto.md @@ -0,0 +1,140 @@ +# *crypto* library + +The library provides common cryptographic functions for mods + +TLS and HTTP are not part of this library. They can use crypto for expensive +operations + +Keys, signatures, hashes and encrypted data are passed as regular Lua strings. +Strings may contain zero bytes + +The current API version is available in `crypto.API_VERSION` + +## Hashing + +```lua +crypto.sha256(data: str) -> str +crypto.sha384(data: str) -> str +crypto.sha512(data: str) -> str +crypto.md5(data: str) -> str +crypto.hash(hash: str, data: str) -> str +crypto.hmac(hash: str, key: str, data: str) -> str +``` + +Supported names are `SHA256`, `SHA384`, `SHA512` and `MD5` + +MD5 is provided for old formats. SHA256 should be used for new data + +Large data can be hashed in parts + +```lua +local hash = crypto.hash_new("SHA256") +hash:update(part1) +hash:update(part2) +local result = hash:final() +``` + +Call `reset` before using the context again + +## Signatures + +```lua +crypto.ed25519_keypair() -> private_key, public_key +crypto.ed25519_public(private_key: str) -> str +crypto.ed25519_sign(private_key: str, message: str) -> str +crypto.ed25519_verify(public_key: str, message: str, signature: str) + -> true + -> false, error + +crypto.ecdsa_keypair(curve: str) -> private_key, public_key +crypto.ecdsa_public(curve: str, private_key: str) -> str +crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str +crypto.ecdsa_verify(curve: str, public_key: str, message: str, + signature: str, hash: str) + -> true + -> false, error + +crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str) + -> true + -> false, error +crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str, salt_length: int) + -> true + -> false, error +``` + +Ed25519 keys are 32 bytes and signatures are 64 bytes + +ECDSA supports `P-256`, `P-384` and `P-521`. Public keys use the uncompressed +SEC1 format and signatures use DER + +RSA modulus and exponent use big endian. A `salt_length` of `-1` uses the hash +size. RSA key generation is not provided, Ed25519 is easier for new keys + +## Key exchange + +```lua +crypto.x25519_keypair() -> private_key, public_key +crypto.x25519_public(private_key: str) -> str +crypto.x25519(private_key: str, peer_public_key: str) -> str +crypto.x25519_shared(private_key: str, peer_public_key: str) -> str + +crypto.p256_keypair() -> private_key, public_key +crypto.p256_public(private_key: str) -> str +crypto.p256_shared(private_key: str, peer_public_key: str) -> str +``` + +`x25519` and `x25519_shared` do the same operation + +Pass the shared secret through HKDF before using it as a key + +## Encryption + +```lua +crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str +crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str) + -> plaintext + -> nil, error + +crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str, + plaintext: str) -> str +crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str, + ciphertext: str) + -> plaintext + -> nil, error +``` + +AES GCM accepts 16 and 32 byte keys + +ChaCha20 Poly1305 uses a 32 byte key and a 12 byte nonce + +A 16 byte tag is appended to ciphertext. Decryption returns nothing when the +tag is invalid + +Never use the same nonce twice with the same key + +## Other functions + +```lua +crypto.random_bytes(length: int) -> str +crypto.constant_time_equal(left: str, right: str) -> bool +crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str +crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str +crypto.pbkdf2(hash: str, password: str, salt: str, + iterations: int, length: int) -> str +crypto.scrypt(password: str, salt: str, n: int, r: int, p: int, + length: int, [optional]max_memory: int=0) -> str +crypto.features() -> table +``` + +Use scrypt or PBKDF2 with a random salt for passwords. A regular SHA256 hash is +not suitable for password storage + +`features` returns the OpenSSL version and available functions + +An invalid signature returns `false, "invalid_signature"` + +An invalid encryption tag returns `nil, "authentication_failed"` + +Invalid arguments raise a regular Lua error diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 1e6d73098..dde26f0b9 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -15,6 +15,7 @@ - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) - [byteutil](scripting/builtins/libbyteutil.md) + - [crypto](scripting/builtins/libcrypto.md) - [cameras](scripting/builtins/libcameras.md) - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) diff --git a/doc/ru/scripting/builtins/libcrypto.md b/doc/ru/scripting/builtins/libcrypto.md new file mode 100644 index 000000000..dce86d401 --- /dev/null +++ b/doc/ru/scripting/builtins/libcrypto.md @@ -0,0 +1,159 @@ +# Библиотека *crypto* + +Библиотека предоставляет основные криптографические функции для модов + +TLS и HTTP в нее не входят. Эти протоколы используют crypto для тяжелых +вычислений + +Все бинарные значения передаются обычными Lua строками. Это касается ключей, +подписей, хешей и зашифрованных данных. Строки могут содержать нулевые байты + +Текущая версия API находится в `crypto.API_VERSION` + +## Хеширование + +```lua +crypto.sha256(data: str) -> str +crypto.sha384(data: str) -> str +crypto.sha512(data: str) -> str +crypto.md5(data: str) -> str + +crypto.hash(hash: str, data: str) -> str +crypto.hmac(hash: str, key: str, data: str) -> str +``` + +Поддерживаются `SHA256`, `SHA384`, `SHA512` и `MD5` + +MD5 нужен для совместимости со старыми форматами. Для новых данных лучше +использовать SHA256 + +Большой файл можно передавать частями + +```lua +local hash = crypto.hash_new("SHA256") + +hash:update(part1) +hash:update(part2) + +local result = hash:final() +``` + +После `final` контекст можно очистить через `reset` + +## Подписи + +```lua +crypto.ed25519_keypair() -> private_key, public_key +crypto.ed25519_public(private_key: str) -> str +crypto.ed25519_sign(private_key: str, message: str) -> str +crypto.ed25519_verify(public_key: str, message: str, signature: str) + -> true + -> false, error +``` + +Ключи Ed25519 имеют длину 32 байта. Подпись имеет длину 64 байта + +```lua +crypto.ecdsa_keypair(curve: str) -> private_key, public_key +crypto.ecdsa_public(curve: str, private_key: str) -> str +crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str +crypto.ecdsa_verify(curve: str, public_key: str, message: str, + signature: str, hash: str) + -> true + -> false, error +``` + +Доступны кривые `P-256`, `P-384` и `P-521` + +Публичный ключ ECDSA записывается как несжатая SEC1 точка. Подпись хранится в +DER формате + +```lua +crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str) + -> true + -> false, error + +crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str, salt_length: int) + -> true + -> false, error +``` + +Модуль и экспонента RSA передаются в big endian. Значение `salt_length = -1` +использует размер хеша + +Генерации RSA ключей в библиотеке нет. Для новых ключей проще использовать +Ed25519 + +## Обмен ключами + +```lua +crypto.x25519_keypair() -> private_key, public_key +crypto.x25519_public(private_key: str) -> str +crypto.x25519(private_key: str, peer_public_key: str) -> str +crypto.x25519_shared(private_key: str, peer_public_key: str) -> str + +crypto.p256_keypair() -> private_key, public_key +crypto.p256_public(private_key: str) -> str +crypto.p256_shared(private_key: str, peer_public_key: str) -> str +``` + +`x25519` и `x25519_shared` выполняют одну и ту же операцию + +Общий секрет не стоит использовать как готовый ключ. Сначала его нужно +обработать через HKDF + +## Шифрование + +```lua +crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str +crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str) + -> plaintext + -> nil, error + +crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str, + plaintext: str) -> str +crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str, + ciphertext: str) + -> plaintext + -> nil, error +``` + +AES GCM принимает ключи размером 16 или 32 байта + +ChaCha20 Poly1305 использует ключ размером 32 байта и nonce размером 12 байт + +В конец зашифрованных данных добавляется тег размером 16 байт. При неверном +теге функция расшифровки ничего не возвращает + +Нельзя повторно использовать один nonce с тем же ключом + +## Остальные функции + +```lua +crypto.random_bytes(length: int) -> str +crypto.constant_time_equal(left: str, right: str) -> bool + +crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str +crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str + +crypto.pbkdf2(hash: str, password: str, salt: str, + iterations: int, length: int) -> str +crypto.scrypt(password: str, salt: str, n: int, r: int, p: int, + length: int, [опционально]max_memory: int=0) -> str + +crypto.features() -> table +``` + +Для хранения паролей следует использовать scrypt или PBKDF2 со случайной +солью. Обычный SHA256 для паролей не подходит + +`features` возвращает версию OpenSSL и список доступных возможностей. Это +можно использовать если мод запускается на разных сборках движка + +Неверная подпись возвращает `false, "invalid_signature"` + +Неверный тег при расшифровке возвращает `nil, "authentication_failed"` + +Ошибки в аргументах вызывают обычную Lua ошибку diff --git a/flake.nix b/flake.nix index ee1e6baa9..ead09d092 100644 --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,7 @@ openal luajit curl + openssl entt mesa freeglut diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12ece0f7f..bbe8a9763 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ endif() find_package(ZLIB REQUIRED) find_package(PNG REQUIRED) find_package(CURL REQUIRED) +find_package(OpenSSL 3.0 REQUIRED COMPONENTS Crypto) find_package(glfw3 REQUIRED) find_package(Freetype REQUIRED) if(NOT APPLE) @@ -79,12 +80,13 @@ target_link_libraries( ZLIB::ZLIB PNG::PNG CURL::libcurl + OpenSSL::Crypto OpenAL::OpenAL Vorbis::vorbis Vorbis::vorbisfile - luajit::luajit Freetype::Freetype - PUBLIC glm::glm # Need public for src/delegates.hpp, which including to + PUBLIC luajit::luajit + glm::glm # Need public for src/delegates.hpp, which including to # main.cpp ) diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp new file mode 100644 index 000000000..169f94d65 --- /dev/null +++ b/src/crypto/Crypto.cpp @@ -0,0 +1,1257 @@ +#include "Crypto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace crypto { + Error::Error(ErrorCode code, const std::string& message) + : std::runtime_error(message), errorCode(code) { + } + + ErrorCode Error::code() const noexcept { + return errorCode; + } + + const char* Error::codeName() const noexcept { + switch (errorCode) { + case ErrorCode::InvalidArgument: + return "invalid_argument"; + case ErrorCode::InvalidKey: + return "invalid_key"; + case ErrorCode::InvalidSignature: + return "invalid_signature"; + case ErrorCode::AuthenticationFailed: + return "authentication_failed"; + case ErrorCode::UnsupportedAlgorithm: + return "unsupported_algorithm"; + case ErrorCode::OutputTooLarge: + return "output_too_large"; + case ErrorCode::InvalidState: + return "invalid_state"; + case ErrorCode::BackendError: + return "backend_error"; + } + return "backend_error"; + } + + namespace { + constexpr std::size_t AEAD_TAG_SIZE = 16; + + template + using Handle = std::unique_ptr; + + using BnHandle = Handle; + using EcGroupHandle = Handle; + using EcPointHandle = Handle; + using EvpCipherCtxHandle = Handle; + using EvpMdCtxHandle = Handle; + using EvpPkeyHandle = Handle; + using EvpPkeyCtxHandle = Handle; + using ParamBldHandle = Handle; + using ParamHandle = Handle; + + const unsigned char* bytes(std::string_view value) { + return reinterpret_cast(value.data()); + } + + std::string opensslError(const std::string& operation) { + const unsigned long code = ERR_get_error(); + if (code == 0) { + return operation; + } + char message[256]; + ERR_error_string_n(code, message, sizeof(message)); + return operation + ": " + message; + } + + [[noreturn]] void fail(const std::string& operation) { + throw Error(ErrorCode::BackendError, opensslError(operation)); + } + + int checkedSize(std::size_t size, const char* name) { + if (size > static_cast(INT_MAX)) { + throw Error( + ErrorCode::OutputTooLarge, + std::string(name) + " is too large" + ); + } + return static_cast(size); + } + + const EVP_MD* getShaDigest(std::string_view hash) { + if (hash == "SHA256") return EVP_sha256(); + if (hash == "SHA384") return EVP_sha384(); + if (hash == "SHA512") return EVP_sha512(); + throw Error( + ErrorCode::UnsupportedAlgorithm, + "unsupported hash: " + std::string(hash) + ); + } + + const EVP_MD* getDigest(std::string_view hash) { + if (hash == "MD5") return EVP_md5(); + return getShaDigest(hash); + } + + const char* curveName(std::string_view curve) { + if (curve == "P-256") return "prime256v1"; + if (curve == "P-384") return "secp384r1"; + if (curve == "P-521") return "secp521r1"; + throw Error( + ErrorCode::UnsupportedAlgorithm, + "unsupported curve: " + std::string(curve) + ); + } + + int curveNid(std::string_view curve) { + if (curve == "P-256") return NID_X9_62_prime256v1; + if (curve == "P-384") return NID_secp384r1; + if (curve == "P-521") return NID_secp521r1; + curveName(curve); + return NID_undef; + } + + std::size_t curvePrivateSize(std::string_view curve) { + if (curve == "P-256") return 32; + if (curve == "P-384") return 48; + if (curve == "P-521") return 66; + curveName(curve); + return 0; + } + + EvpPkeyHandle fromData( + const char* algorithm, + int selection, + OSSL_PARAM_BLD* builder, + ErrorCode failureCode = ErrorCode::BackendError + ) { + ParamHandle params( + OSSL_PARAM_BLD_to_param(builder), OSSL_PARAM_free + ); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_from_name(nullptr, algorithm, nullptr), + EVP_PKEY_CTX_free + ); + EVP_PKEY* rawKey = nullptr; + if (!params || !ctx || EVP_PKEY_fromdata_init(ctx.get()) <= 0 || + EVP_PKEY_fromdata( + ctx.get(), &rawKey, selection, params.get() + ) <= 0) { + throw Error( + failureCode, + opensslError( + std::string("failed to import ") + algorithm + " key" + ) + ); + } + return EvpPkeyHandle(rawKey, EVP_PKEY_free); + } + + void checkPublicKey(EVP_PKEY* key, const char* name) { + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key, nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_public_check(ctx.get()) != 1) { + throw Error( + ErrorCode::InvalidKey, + opensslError(std::string("invalid ") + name + " public key") + ); + } + } + + EvpPkeyHandle makeEcdsaPublicKey( + std::string_view curve, std::string_view encoded + ) { + if (encoded.empty() || + static_cast(encoded[0]) != 0x04) { + throw Error( + ErrorCode::InvalidKey, + "ECDSA public key must be an uncompressed SEC1 point" + ); + } + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + const char* group = curveName(curve); + if (!builder || + OSSL_PARAM_BLD_push_utf8_string( + builder.get(), OSSL_PKEY_PARAM_GROUP_NAME, group, 0 + ) != 1 || + OSSL_PARAM_BLD_push_octet_string( + builder.get(), + OSSL_PKEY_PARAM_PUB_KEY, + encoded.data(), + encoded.size() + ) != 1) { + fail("failed to prepare ECDSA public key"); + } + auto result = fromData( + "EC", EVP_PKEY_PUBLIC_KEY, builder.get(), ErrorCode::InvalidKey + ); + checkPublicKey(result.get(), "ECDSA"); + return result; + } + + EvpPkeyHandle makeRsaPublicKey( + std::string_view modulus, std::string_view exponent + ) { + if (modulus.empty() || exponent.empty()) { + throw Error( + ErrorCode::InvalidKey, + "RSA modulus and exponent must not be empty" + ); + } + BnHandle n( + BN_bin2bn( + bytes(modulus), + checkedSize(modulus.size(), "modulus"), + nullptr + ), + BN_free + ); + BnHandle e( + BN_bin2bn( + bytes(exponent), + checkedSize(exponent.size(), "exponent"), + nullptr + ), + BN_free + ); + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + if (!n || !e || !builder) { + fail("failed to prepare RSA public key"); + } + if (BN_is_zero(n.get()) || BN_is_zero(e.get())) { + throw Error(ErrorCode::InvalidKey, "invalid RSA public key"); + } + if (OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_RSA_N, n.get() + ) != 1 || + OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_RSA_E, e.get() + ) != 1) { + fail("invalid RSA public key"); + } + auto result = fromData( + "RSA", EVP_PKEY_PUBLIC_KEY, builder.get(), ErrorCode::InvalidKey + ); + checkPublicKey(result.get(), "RSA"); + return result; + } + + bool verifyDigestSignature( + EVP_PKEY* key, + const EVP_MD* md, + std::string_view message, + std::string_view signature, + int rsaPadding, + int saltLength + ) { + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + EVP_PKEY_CTX* pkeyCtx = nullptr; + if (!ctx || + EVP_DigestVerifyInit(ctx.get(), &pkeyCtx, md, nullptr, key) != + 1) { + fail("signature verification initialization failed"); + } + if (rsaPadding != 0) { + if (EVP_PKEY_CTX_set_rsa_padding(pkeyCtx, rsaPadding) <= 0) { + fail("failed to configure RSA padding"); + } + if (rsaPadding == RSA_PKCS1_PSS_PADDING && + EVP_PKEY_CTX_set_rsa_pss_saltlen(pkeyCtx, saltLength) <= + 0) { + fail("failed to configure RSA-PSS salt length"); + } + } + if (EVP_DigestVerifyUpdate( + ctx.get(), message.data(), message.size() + ) != 1) { + fail("signature verification update failed"); + } + const int result = EVP_DigestVerifyFinal( + ctx.get(), bytes(signature), signature.size() + ); + if (result < 0) { + throw Error( + ErrorCode::InvalidSignature, + opensslError("invalid signature encoding") + ); + } + if (result == 0) ERR_clear_error(); + return result == 1; + } + + EvpPkeyHandle rawPrivateKey( + int type, std::string_view key, const char* name + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, + std::string(name) + " private key must be 32 bytes" + ); + } + EvpPkeyHandle result( + EVP_PKEY_new_raw_private_key( + type, nullptr, bytes(key), key.size() + ), + EVP_PKEY_free + ); + if (!result) fail(std::string("invalid ") + name + " private key"); + return result; + } + + EvpPkeyHandle rawPublicKey( + int type, std::string_view key, const char* name + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, + std::string(name) + " public key must be 32 bytes" + ); + } + EvpPkeyHandle result( + EVP_PKEY_new_raw_public_key( + type, nullptr, bytes(key), key.size() + ), + EVP_PKEY_free + ); + if (!result) fail(std::string("invalid ") + name + " public key"); + return result; + } + + const EVP_CIPHER* aesGcmCipher(std::size_t keySize) { + if (keySize == 16) return EVP_aes_128_gcm(); + if (keySize == 32) return EVP_aes_256_gcm(); + throw Error( + ErrorCode::InvalidKey, "AES-GCM key must be 16 or 32 bytes" + ); + } + + Bytes aeadEncrypt( + const EVP_CIPHER* cipher, + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + if (nonce.empty()) { + throw Error( + ErrorCode::InvalidArgument, "nonce must not be empty" + ); + } + const int nonceSize = checkedSize(nonce.size(), "nonce"); + const int aadSize = checkedSize(aad.size(), "aad"); + const int plaintextSize = + checkedSize(plaintext.size(), "plaintext"); + EvpCipherCtxHandle ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (!ctx || + EVP_EncryptInit_ex( + ctx.get(), cipher, nullptr, nullptr, nullptr + ) != 1 || + EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, nonceSize, nullptr + ) != 1 || + EVP_EncryptInit_ex( + ctx.get(), nullptr, nullptr, bytes(key), bytes(nonce) + ) != 1) { + fail("AEAD encryption initialization failed"); + } + int written = 0; + if (!aad.empty() && + EVP_EncryptUpdate( + ctx.get(), nullptr, &written, bytes(aad), aadSize + ) != 1) { + fail("AEAD AAD processing failed"); + } + Bytes output(plaintext.size() + AEAD_TAG_SIZE); + int outputSize = 0; + if (!plaintext.empty() && EVP_EncryptUpdate( + ctx.get(), + output.data(), + &outputSize, + bytes(plaintext), + plaintextSize + ) != 1) { + fail("AEAD encryption failed"); + } + if (EVP_EncryptFinal_ex( + ctx.get(), output.data() + outputSize, &written + ) != 1) { + fail("AEAD encryption finalization failed"); + } + outputSize += written; + if (EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_GET_TAG, + AEAD_TAG_SIZE, + output.data() + outputSize + ) != 1) { + fail("AEAD tag retrieval failed"); + } + output.resize(static_cast(outputSize) + AEAD_TAG_SIZE); + return output; + } + + Bytes aeadDecrypt( + const EVP_CIPHER* cipher, + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + if (nonce.empty()) { + throw Error( + ErrorCode::InvalidArgument, "nonce must not be empty" + ); + } + if (ciphertextWithTag.size() < AEAD_TAG_SIZE) { + throw Error( + ErrorCode::AuthenticationFailed, + "ciphertext is shorter than the authentication tag" + ); + } + const std::size_t ciphertextSize = + ciphertextWithTag.size() - AEAD_TAG_SIZE; + const int nonceSize = checkedSize(nonce.size(), "nonce"); + const int aadSize = checkedSize(aad.size(), "aad"); + const int inputSize = checkedSize(ciphertextSize, "ciphertext"); + EvpCipherCtxHandle ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (!ctx || + EVP_DecryptInit_ex( + ctx.get(), cipher, nullptr, nullptr, nullptr + ) != 1 || + EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, nonceSize, nullptr + ) != 1 || + EVP_DecryptInit_ex( + ctx.get(), nullptr, nullptr, bytes(key), bytes(nonce) + ) != 1) { + fail("AEAD decryption initialization failed"); + } + int written = 0; + if (!aad.empty() && + EVP_DecryptUpdate( + ctx.get(), nullptr, &written, bytes(aad), aadSize + ) != 1) { + fail("AEAD AAD processing failed"); + } + Bytes output(ciphertextSize + AEAD_TAG_SIZE); + int outputSize = 0; + if (ciphertextSize != 0 && EVP_DecryptUpdate( + ctx.get(), + output.data(), + &outputSize, + bytes(ciphertextWithTag), + inputSize + ) != 1) { + fail("AEAD decryption failed"); + } + if (EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_SET_TAG, + AEAD_TAG_SIZE, + const_cast( + bytes(ciphertextWithTag) + ciphertextSize + ) + ) != 1) { + fail("AEAD tag setup failed"); + } + const int result = EVP_DecryptFinal_ex( + ctx.get(), output.data() + outputSize, &written + ); + if (result != 1) { + ERR_clear_error(); + throw Error( + ErrorCode::AuthenticationFailed, "authentication failed" + ); + } + outputSize += written; + output.resize(static_cast(outputSize)); + return output; + } + + struct EcKey { + EvpPkeyHandle key; + Bytes publicKey; + }; + + EcKey ecPrivateKey( + std::string_view curve, std::string_view privateKey + ) { + const std::size_t expectedSize = curvePrivateSize(curve); + if (privateKey.size() != expectedSize) { + throw Error( + ErrorCode::InvalidKey, + std::string(curve) + " private key must be " + + std::to_string(expectedSize) + " bytes" + ); + } + EcGroupHandle group( + EC_GROUP_new_by_curve_name(curveNid(curve)), EC_GROUP_free + ); + BnHandle scalar( + BN_bin2bn( + bytes(privateKey), + checkedSize(privateKey.size(), "private key"), + nullptr + ), + BN_free + ); + BnHandle order(BN_new(), BN_free); + if (!group || !scalar || !order || BN_is_zero(scalar.get()) || + EC_GROUP_get_order(group.get(), order.get(), nullptr) != 1 || + BN_cmp(scalar.get(), order.get()) >= 0) { + throw Error(ErrorCode::InvalidKey, "invalid EC private key"); + } + EcPointHandle publicPoint(EC_POINT_new(group.get()), EC_POINT_free); + if (!publicPoint || EC_POINT_mul( + group.get(), + publicPoint.get(), + scalar.get(), + nullptr, + nullptr, + nullptr + ) != 1) { + fail("EC public key derivation failed"); + } + const std::size_t publicSize = EC_POINT_point2oct( + group.get(), + publicPoint.get(), + POINT_CONVERSION_UNCOMPRESSED, + nullptr, + 0, + nullptr + ); + Bytes publicKey(publicSize); + if (publicSize == 0 || EC_POINT_point2oct( + group.get(), + publicPoint.get(), + POINT_CONVERSION_UNCOMPRESSED, + publicKey.data(), + publicKey.size(), + nullptr + ) != publicSize) { + fail("EC public key encoding failed"); + } + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + if (!builder || + OSSL_PARAM_BLD_push_utf8_string( + builder.get(), + OSSL_PKEY_PARAM_GROUP_NAME, + curveName(curve), + 0 + ) != 1 || + OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_PRIV_KEY, scalar.get() + ) != 1 || + OSSL_PARAM_BLD_push_octet_string( + builder.get(), + OSSL_PKEY_PARAM_PUB_KEY, + publicKey.data(), + publicKey.size() + ) != 1) { + fail("failed to prepare EC private key"); + } + return { + fromData("EC", EVP_PKEY_KEYPAIR, builder.get()), + std::move(publicKey) + }; + } + + KeyPair rawKeyPair(const char* algorithm) { + EvpPkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, algorithm), EVP_PKEY_free + ); + if (!key) fail(std::string(algorithm) + " key generation failed"); + KeyPair result {Bytes(32), Bytes(32)}; + std::size_t privateSize = result.privateKey.size(); + std::size_t publicSize = result.publicKey.size(); + if (EVP_PKEY_get_raw_private_key( + key.get(), result.privateKey.data(), &privateSize + ) != 1 || + EVP_PKEY_get_raw_public_key( + key.get(), result.publicKey.data(), &publicSize + ) != 1) { + fail(std::string(algorithm) + " key export failed"); + } + result.privateKey.resize(privateSize); + result.publicKey.resize(publicSize); + return result; + } + + Bytes rawPublicFromPrivate( + int type, std::string_view privateKey, const char* name + ) { + auto key = rawPrivateKey(type, privateKey, name); + Bytes result(32); + std::size_t size = result.size(); + if (EVP_PKEY_get_raw_public_key(key.get(), result.data(), &size) != + 1) { + fail(std::string(name) + " public key derivation failed"); + } + result.resize(size); + return result; + } + + Bytes signDigest( + EVP_PKEY* key, const EVP_MD* md, std::string_view message + ) { + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || + EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, key) != 1 || + EVP_DigestSignUpdate( + ctx.get(), message.data(), message.size() + ) != 1) { + fail("signature initialization failed"); + } + std::size_t size = 0; + if (EVP_DigestSignFinal(ctx.get(), nullptr, &size) != 1) { + fail("signature size query failed"); + } + Bytes signature(size); + if (EVP_DigestSignFinal(ctx.get(), signature.data(), &size) != 1) { + fail("signing failed"); + } + signature.resize(size); + return signature; + } + } + + struct HashContext::Impl { + explicit Impl(std::string_view name) + : digest(getDigest(name)), ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free) { + if (!ctx || EVP_DigestInit_ex(ctx.get(), digest, nullptr) != 1) { + fail("hash context initialization failed"); + } + } + + const EVP_MD* digest; + EvpMdCtxHandle ctx; + bool finalized = false; + }; + + HashContext::HashContext(std::string_view hash) + : impl(std::make_unique(hash)) { + } + + HashContext::~HashContext() = default; + HashContext::HashContext(HashContext&&) noexcept = default; + HashContext& HashContext::operator=(HashContext&&) noexcept = default; + + void HashContext::update(std::string_view data) { + if (impl->finalized) { + throw Error(ErrorCode::InvalidState, "hash context is finalized"); + } + if (EVP_DigestUpdate(impl->ctx.get(), data.data(), data.size()) != 1) { + fail("hash update failed"); + } + } + + Bytes HashContext::final() { + if (impl->finalized) { + throw Error(ErrorCode::InvalidState, "hash context is finalized"); + } + Bytes output(static_cast(EVP_MD_size(impl->digest))); + unsigned int size = 0; + if (EVP_DigestFinal_ex(impl->ctx.get(), output.data(), &size) != 1) { + fail("hash finalization failed"); + } + impl->finalized = true; + output.resize(size); + return output; + } + + void HashContext::reset() { + if (EVP_DigestInit_ex(impl->ctx.get(), impl->digest, nullptr) != 1) { + fail("hash reset failed"); + } + impl->finalized = false; + } + + Bytes digest(std::string_view hash, std::string_view data) { + const EVP_MD* md = getDigest(hash); + Bytes output(static_cast(EVP_MD_size(md))); + unsigned int size = 0; + if (EVP_Digest( + data.data(), data.size(), output.data(), &size, md, nullptr + ) != 1) { + fail("digest calculation failed"); + } + output.resize(size); + return output; + } + + Bytes hmac( + std::string_view hash, std::string_view key, std::string_view data + ) { + const EVP_MD* md = getShaDigest(hash); + Bytes output(static_cast(EVP_MD_size(md))); + unsigned int size = 0; + if (!HMAC( + md, + key.data(), + checkedSize(key.size(), "HMAC key"), + bytes(data), + data.size(), + output.data(), + &size + )) { + fail("HMAC calculation failed"); + } + output.resize(size); + return output; + } + + KeyPair ed25519KeyPair() { + return rawKeyPair("ED25519"); + } + + Bytes ed25519Public(std::string_view privateKey) { + return rawPublicFromPrivate(EVP_PKEY_ED25519, privateKey, "Ed25519"); + } + + Bytes ed25519Sign(std::string_view privateKey, std::string_view message) { + auto key = rawPrivateKey(EVP_PKEY_ED25519, privateKey, "Ed25519"); + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || EVP_DigestSignInit( + ctx.get(), nullptr, nullptr, nullptr, key.get() + ) != 1) { + fail("Ed25519 signing initialization failed"); + } + std::size_t size = 0; + if (EVP_DigestSign( + ctx.get(), nullptr, &size, bytes(message), message.size() + ) != 1) { + fail("Ed25519 signature size query failed"); + } + Bytes signature(size); + if (EVP_DigestSign( + ctx.get(), + signature.data(), + &size, + bytes(message), + message.size() + ) != 1) { + fail("Ed25519 signing failed"); + } + signature.resize(size); + return signature; + } + + bool ed25519Verify( + std::string_view publicKey, + std::string_view message, + std::string_view signature + ) { + if (signature.size() != 64) { + throw Error( + ErrorCode::InvalidSignature, + "Ed25519 signature must be 64 bytes" + ); + } + auto key = rawPublicKey(EVP_PKEY_ED25519, publicKey, "Ed25519"); + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || EVP_DigestVerifyInit( + ctx.get(), nullptr, nullptr, nullptr, key.get() + ) != 1) { + fail("Ed25519 verification initialization failed"); + } + const int result = EVP_DigestVerify( + ctx.get(), + bytes(signature), + signature.size(), + bytes(message), + message.size() + ); + if (result < 0) { + throw Error( + ErrorCode::InvalidSignature, + opensslError("invalid Ed25519 signature") + ); + } + if (result == 0) ERR_clear_error(); + return result == 1; + } + + KeyPair ecdsaKeyPair(std::string_view curve) { + const std::size_t privateSize = curvePrivateSize(curve); + EvpPkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "EC", curveName(curve)), + EVP_PKEY_free + ); + if (!key) fail("ECDSA key generation failed"); + BIGNUM* rawPrivate = nullptr; + if (EVP_PKEY_get_bn_param( + key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &rawPrivate + ) != 1) { + fail("ECDSA private key export failed"); + } + BnHandle privateKey(rawPrivate, BN_free); + KeyPair result {Bytes(privateSize), {}}; + if (BN_bn2binpad( + privateKey.get(), + result.privateKey.data(), + checkedSize(privateSize, "private key") + ) != static_cast(privateSize)) { + fail("ECDSA private key encoding failed"); + } + std::size_t publicSize = 0; + if (EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicSize + ) != 1) { + fail("ECDSA public key size query failed"); + } + result.publicKey.resize(publicSize); + if (EVP_PKEY_get_octet_string_param( + key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + result.publicKey.data(), + result.publicKey.size(), + &publicSize + ) != 1) { + fail("ECDSA public key export failed"); + } + result.publicKey.resize(publicSize); + return result; + } + + Bytes ecdsaPublic(std::string_view curve, std::string_view privateKey) { + return ecPrivateKey(curve, privateKey).publicKey; + } + + Bytes ecdsaSign( + std::string_view curve, + std::string_view privateKey, + std::string_view message, + std::string_view hash + ) { + auto key = ecPrivateKey(curve, privateKey); + return signDigest(key.key.get(), getShaDigest(hash), message); + } + + bool ecdsaVerify( + std::string_view curve, + std::string_view publicKey, + std::string_view message, + std::string_view signature, + std::string_view hash + ) { + auto key = makeEcdsaPublicKey(curve, publicKey); + return verifyDigestSignature( + key.get(), getShaDigest(hash), message, signature, 0, 0 + ); + } + + bool rsaPkcs1Verify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature + ) { + auto key = makeRsaPublicKey(modulus, exponent); + return verifyDigestSignature( + key.get(), + getShaDigest(hash), + message, + signature, + RSA_PKCS1_PADDING, + 0 + ); + } + + bool rsaPssVerify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature, + int saltLength + ) { + if (saltLength < -1) { + throw Error( + ErrorCode::InvalidArgument, + "RSA-PSS salt length must be -1 or non-negative" + ); + } + auto key = makeRsaPublicKey(modulus, exponent); + return verifyDigestSignature( + key.get(), + getShaDigest(hash), + message, + signature, + RSA_PKCS1_PSS_PADDING, + saltLength == -1 ? RSA_PSS_SALTLEN_DIGEST : saltLength + ); + } + + KeyPair x25519KeyPair() { + return rawKeyPair("X25519"); + } + + Bytes x25519Public(std::string_view privateKey) { + return rawPublicFromPrivate(EVP_PKEY_X25519, privateKey, "X25519"); + } + + Bytes x25519(std::string_view privateKey, std::string_view peerPublicKey) { + auto key = rawPrivateKey(EVP_PKEY_X25519, privateKey, "X25519"); + auto peer = rawPublicKey(EVP_PKEY_X25519, peerPublicKey, "X25519"); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key.get(), nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_derive_set_peer(ctx.get(), peer.get()) != 1) { + fail("X25519 key exchange initialization failed"); + } + Bytes output(32); + std::size_t size = output.size(); + if (EVP_PKEY_derive(ctx.get(), output.data(), &size) != 1) { + fail("X25519 key exchange failed"); + } + output.resize(size); + const Bytes zero(output.size(), 0); + if (constantTimeEqual( + std::string_view( + reinterpret_cast(output.data()), output.size() + ), + std::string_view( + reinterpret_cast(zero.data()), zero.size() + ) + )) { + throw Error( + ErrorCode::InvalidKey, + "X25519 produced an all-zero shared secret" + ); + } + return output; + } + + Bytes p256Public(std::string_view privateKey) { + return ecPrivateKey("P-256", privateKey).publicKey; + } + + Bytes p256Shared( + std::string_view privateKey, std::string_view peerPublicKey + ) { + if (peerPublicKey.size() != 65 || + static_cast(peerPublicKey[0]) != 0x04) { + throw Error( + ErrorCode::InvalidKey, + "P-256 public key must be a 65-byte uncompressed SEC1 point" + ); + } + auto key = ecPrivateKey("P-256", privateKey); + auto peer = makeEcdsaPublicKey("P-256", peerPublicKey); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key.key.get(), nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer(ctx.get(), peer.get()) <= 0) { + fail("P-256 key exchange initialization failed"); + } + std::size_t size = 0; + if (EVP_PKEY_derive(ctx.get(), nullptr, &size) <= 0) { + fail("P-256 shared secret size query failed"); + } + Bytes output(size); + if (EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0) { + fail("P-256 key exchange failed"); + } + output.resize(size); + return output; + } + + Bytes aesGcmEncrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + return aeadEncrypt( + aesGcmCipher(key.size()), key, nonce, aad, plaintext + ); + } + + Bytes aesGcmDecrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + return aeadDecrypt( + aesGcmCipher(key.size()), key, nonce, aad, ciphertextWithTag + ); + } + + Bytes chacha20Poly1305Encrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, "ChaCha20-Poly1305 key must be 32 bytes" + ); + } + if (nonce.size() != 12) { + throw Error( + ErrorCode::InvalidArgument, + "ChaCha20-Poly1305 nonce must be 12 bytes" + ); + } + return aeadEncrypt(EVP_chacha20_poly1305(), key, nonce, aad, plaintext); + } + + Bytes chacha20Poly1305Decrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, "ChaCha20-Poly1305 key must be 32 bytes" + ); + } + if (nonce.size() != 12) { + throw Error( + ErrorCode::InvalidArgument, + "ChaCha20-Poly1305 nonce must be 12 bytes" + ); + } + return aeadDecrypt( + EVP_chacha20_poly1305(), key, nonce, aad, ciphertextWithTag + ); + } + + Bytes randomBytes(std::size_t length) { + checkedSize(length, "random byte count"); + Bytes output(length); + if (length != 0 && + RAND_bytes(output.data(), static_cast(length)) != 1) { + fail("secure random generation failed"); + } + return output; + } + + bool constantTimeEqual(std::string_view left, std::string_view right) { + return left.size() == right.size() && + (left.empty() || + CRYPTO_memcmp(left.data(), right.data(), left.size()) == 0); + } + + Bytes hkdfExtract( + std::string_view hash, std::string_view salt, std::string_view ikm + ) { + const EVP_MD* md = getShaDigest(hash); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), EVP_PKEY_CTX_free + ); + Bytes output(static_cast(EVP_MD_size(md))); + std::size_t size = output.size(); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_CTX_set_hkdf_mode( + ctx.get(), EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY + ) <= 0 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) <= 0 || + EVP_PKEY_CTX_set1_hkdf_salt( + ctx.get(), bytes(salt), checkedSize(salt.size(), "HKDF salt") + ) <= 0 || + EVP_PKEY_CTX_set1_hkdf_key( + ctx.get(), + bytes(ikm), + checkedSize(ikm.size(), "HKDF input key material") + ) <= 0 || + EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0) { + fail("HKDF extract failed"); + } + output.resize(size); + return output; + } + + Bytes hkdfExpand( + std::string_view hash, + std::string_view prk, + std::string_view info, + std::size_t length + ) { + const EVP_MD* md = getShaDigest(hash); + const std::size_t maxLength = + static_cast(EVP_MD_size(md)) * 255; + if (length > maxLength) { + throw Error( + ErrorCode::OutputTooLarge, + "HKDF output exceeds 255 digest blocks" + ); + } + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), EVP_PKEY_CTX_free + ); + Bytes output(length); + std::size_t size = output.size(); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_CTX_set_hkdf_mode( + ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY + ) <= 0 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) <= 0 || + EVP_PKEY_CTX_set1_hkdf_key( + ctx.get(), + bytes(prk), + checkedSize(prk.size(), "HKDF pseudorandom key") + ) <= 0 || + (!info.empty() && + EVP_PKEY_CTX_add1_hkdf_info( + ctx.get(), bytes(info), checkedSize(info.size(), "HKDF info") + ) <= 0) || + (length != 0 && + EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0)) { + fail("HKDF expand failed"); + } + output.resize(size); + return output; + } + + Bytes pbkdf2( + std::string_view hash, + std::string_view password, + std::string_view salt, + std::uint32_t iterations, + std::size_t length + ) { + if (iterations == 0 || + iterations > static_cast(INT_MAX)) { + throw Error( + ErrorCode::InvalidArgument, + "PBKDF2 iterations must be between 1 and INT_MAX" + ); + } + const EVP_MD* md = getShaDigest(hash); + const int outputSize = checkedSize(length, "PBKDF2 output"); + Bytes output(length); + if (length != 0 && PKCS5_PBKDF2_HMAC( + password.data(), + checkedSize(password.size(), "password"), + bytes(salt), + checkedSize(salt.size(), "salt"), + static_cast(iterations), + md, + outputSize, + output.data() + ) != 1) { + fail("PBKDF2 failed"); + } + return output; + } + + Bytes scrypt( + std::string_view password, + std::string_view salt, + std::uint64_t n, + std::uint64_t r, + std::uint64_t p, + std::size_t length, + std::uint64_t maxMemory + ) { + if (n <= 1 || (n & (n - 1)) != 0 || r == 0 || p == 0) { + throw Error( + ErrorCode::InvalidArgument, + "scrypt requires power-of-two N > 1 and positive r and p" + ); + } + checkedSize(length, "scrypt output"); + Bytes output(length); + if (length != 0 && EVP_PBE_scrypt( + password.data(), + password.size(), + bytes(salt), + salt.size(), + n, + r, + p, + maxMemory, + output.data(), + output.size() + ) != 1) { + fail("scrypt failed"); + } + return output; + } + + const char* backendVersion() { + return OpenSSL_version(OPENSSL_VERSION); + } + + Features features() { + auto hasDigest = [](const char* name) { + EVP_MD* algorithm = EVP_MD_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_MD_free(algorithm); + ERR_clear_error(); + return available; + }; + auto hasCipher = [](const char* name) { + EVP_CIPHER* algorithm = EVP_CIPHER_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_CIPHER_free(algorithm); + ERR_clear_error(); + return available; + }; + auto hasKeyAlgorithm = [](const char* name) { + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_from_name(nullptr, name, nullptr), + EVP_PKEY_CTX_free + ); + const bool available = ctx != nullptr; + ERR_clear_error(); + return available; + }; + auto hasKdf = [](const char* name) { + EVP_KDF* algorithm = EVP_KDF_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_KDF_free(algorithm); + ERR_clear_error(); + return available; + }; + EVP_MAC* hmacAlgorithm = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + const bool hasHmac = hmacAlgorithm != nullptr; + EVP_MAC_free(hmacAlgorithm); + ERR_clear_error(); + const bool hasEc = hasKeyAlgorithm("EC"); + return { + hasDigest("SHA256"), + hasDigest("SHA384"), + hasDigest("SHA512"), + hasDigest("MD5"), + hasHmac, + hasKeyAlgorithm("ED25519"), + hasEc, + hasKeyAlgorithm("RSA"), + hasKeyAlgorithm("X25519"), + hasEc, + hasCipher("AES-128-GCM"), + hasCipher("AES-256-GCM"), + hasCipher("CHACHA20-POLY1305"), + RAND_status() == 1, + hasKdf("HKDF"), + hasKdf("PBKDF2"), + hasKdf("SCRYPT") + }; + } +} diff --git a/src/crypto/Crypto.hpp b/src/crypto/Crypto.hpp new file mode 100644 index 000000000..edcdcea6b --- /dev/null +++ b/src/crypto/Crypto.hpp @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace crypto { + constexpr int API_VERSION = 1; + using Bytes = std::vector; + + enum class ErrorCode { + InvalidArgument, + InvalidKey, + InvalidSignature, + AuthenticationFailed, + UnsupportedAlgorithm, + OutputTooLarge, + InvalidState, + BackendError + }; + + class Error : public std::runtime_error { + public: + Error(ErrorCode code, const std::string& message); + ErrorCode code() const noexcept; + const char* codeName() const noexcept; + private: + ErrorCode errorCode; + }; + + struct KeyPair { + Bytes privateKey; + Bytes publicKey; + }; + + struct Features { + bool sha256; + bool sha384; + bool sha512; + bool md5; + bool hmac; + bool ed25519; + bool ecdsa; + bool rsa; + bool x25519; + bool p256; + bool aes128Gcm; + bool aes256Gcm; + bool chacha20Poly1305; + bool random; + bool hkdf; + bool pbkdf2; + bool scrypt; + }; + + class HashContext { + public: + explicit HashContext(std::string_view hash); + ~HashContext(); + HashContext(HashContext&&) noexcept; + HashContext& operator=(HashContext&&) noexcept; + HashContext(const HashContext&) = delete; + HashContext& operator=(const HashContext&) = delete; + + void update(std::string_view data); + Bytes final(); + void reset(); + private: + struct Impl; + std::unique_ptr impl; + }; + + Bytes digest(std::string_view hash, std::string_view data); + Bytes hmac( + std::string_view hash, std::string_view key, std::string_view data + ); + + KeyPair ed25519KeyPair(); + Bytes ed25519Public(std::string_view privateKey); + Bytes ed25519Sign(std::string_view privateKey, std::string_view message); + + bool ed25519Verify( + std::string_view publicKey, + std::string_view message, + std::string_view signature + ); + bool ecdsaVerify( + std::string_view curve, + std::string_view publicKey, + std::string_view message, + std::string_view signature, + std::string_view hash + ); + KeyPair ecdsaKeyPair(std::string_view curve); + Bytes ecdsaPublic(std::string_view curve, std::string_view privateKey); + Bytes ecdsaSign( + std::string_view curve, + std::string_view privateKey, + std::string_view message, + std::string_view hash + ); + bool rsaPkcs1Verify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature + ); + bool rsaPssVerify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature, + int saltLength + ); + + KeyPair x25519KeyPair(); + Bytes x25519Public(std::string_view privateKey); + Bytes x25519(std::string_view privateKey, std::string_view peerPublicKey); + Bytes p256Public(std::string_view privateKey); + Bytes p256Shared( + std::string_view privateKey, std::string_view peerPublicKey + ); + + Bytes aesGcmEncrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ); + Bytes aesGcmDecrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ); + Bytes chacha20Poly1305Encrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ); + Bytes chacha20Poly1305Decrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ); + + Bytes randomBytes(std::size_t length); + bool constantTimeEqual(std::string_view left, std::string_view right); + Bytes hkdfExtract( + std::string_view hash, std::string_view salt, std::string_view ikm + ); + Bytes hkdfExpand( + std::string_view hash, + std::string_view prk, + std::string_view info, + std::size_t length + ); + Bytes pbkdf2( + std::string_view hash, + std::string_view password, + std::string_view salt, + std::uint32_t iterations, + std::size_t length + ); + Bytes scrypt( + std::string_view password, + std::string_view salt, + std::uint64_t n, + std::uint64_t r, + std::uint64_t p, + std::size_t length, + std::uint64_t maxMemory = 0 + ); + + const char* backendVersion(); + Features features(); +} diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index 965cd0f9c..727265a3a 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -25,6 +25,7 @@ extern const luaL_Reg byteutillib[]; extern const luaL_Reg cameralib[]; extern const luaL_Reg consolelib[]; extern const luaL_Reg corelib[]; +extern const luaL_Reg cryptolib[]; extern const luaL_Reg entitylib[]; extern const luaL_Reg filelib[]; extern const luaL_Reg generationlib[]; @@ -93,3 +94,4 @@ namespace lua { } void initialize_libs_extends(lua::State* L); +void initialize_cryptolib(lua::State* L); diff --git a/src/logic/scripting/lua/libs/libcrypto.cpp b/src/logic/scripting/lua/libs/libcrypto.cpp new file mode 100644 index 000000000..0b60888d7 --- /dev/null +++ b/src/logic/scripting/lua/libs/libcrypto.cpp @@ -0,0 +1,540 @@ +#include +#include +#include +#include +#include +#include + +#include "api_lua.hpp" +#include "crypto/Crypto.hpp" + +namespace { + using HashContext = crypto::HashContext; + + constexpr const char* HASH_CONTEXT_METATABLE = + "voxelcore.crypto.HashContext"; + + int pushBytes(lua::State* L, const crypto::Bytes& value) { + return lua::pushlstring(L, value.data(), value.size()); + } + + int pushFailure(lua::State* L, bool nil, const char* error) { + nil ? lua::pushnil(L) : lua::pushboolean(L, false); + lua::pushstring(L, error); + return 2; + } + + int pushKeyPair(lua::State* L, const crypto::KeyPair& keyPair) { + pushBytes(L, keyPair.privateKey); + pushBytes(L, keyPair.publicKey); + return 2; + } + + template + int verify(lua::State* L, Operation operation) { + try { + if (operation()) return lua::pushboolean(L, true); + return pushFailure(L, false, "invalid_signature"); + } catch (const crypto::Error& error) { + return pushFailure(L, false, error.codeName()); + } catch (const std::exception& error) { + return pushFailure(L, false, "backend_error"); + } + } + + template + int decrypt(lua::State* L, Operation operation) { + try { + return pushBytes(L, operation()); + } catch (const crypto::Error& error) { + return pushFailure(L, true, error.codeName()); + } catch (const std::exception& error) { + return pushFailure(L, true, "backend_error"); + } + } + + std::size_t requireSize(lua::State* L, int index, const char* name) { + if (lua::type(L, index) != LUA_TNUMBER) { + throw std::runtime_error(std::string(name) + " must be a number"); + } + const lua::Number number = lua::tonumber(L, index); + const lua::Integer value = lua::tointeger(L, index); + if (number != static_cast(value)) { + throw std::runtime_error(std::string(name) + " must be an integer"); + } + if (value < 0) { + throw std::runtime_error( + std::string(name) + " must be non-negative" + ); + } + return static_cast(value); + } + + int requireInt(lua::State* L, int index, const char* name) { + if (lua::type(L, index) != LUA_TNUMBER) { + throw std::runtime_error(std::string(name) + " must be a number"); + } + const lua::Number number = lua::tonumber(L, index); + const lua::Integer value = lua::tointeger(L, index); + if (number != static_cast(value)) { + throw std::runtime_error(std::string(name) + " must be an integer"); + } + if (value < INT_MIN || value > INT_MAX) { + throw std::runtime_error(std::string(name) + " is out of range"); + } + return static_cast(value); + } + + int hash(lua::State* L, std::string_view name) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::digest(name, lua::require_lstring(L, 1))); + } + + int l_sha256(lua::State* L) { + return hash(L, "SHA256"); + } + int l_sha384(lua::State* L) { + return hash(L, "SHA384"); + } + int l_sha512(lua::State* L) { + return hash(L, "SHA512"); + } + int l_md5(lua::State* L) { + return hash(L, "MD5"); + } + + int l_hash(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::digest( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + crypto::HashContext* requireHashContext(lua::State* L) { + return static_cast( + luaL_checkudata(L, 1, HASH_CONTEXT_METATABLE) + ); + } + + int l_hash_context_gc(lua::State* L) { + requireHashContext(L)->~HashContext(); + return 0; + } + + int l_hash_context_update(lua::State* L) { + lua::check_argc(L, 2); + requireHashContext(L)->update(lua::require_lstring(L, 2)); + lua::pushvalue(L, 1); + return 1; + } + + int l_hash_context_final(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, requireHashContext(L)->final()); + } + + int l_hash_context_reset(lua::State* L) { + lua::check_argc(L, 1); + requireHashContext(L)->reset(); + lua::pushvalue(L, 1); + return 1; + } + + void ensureHashContextMetatable(lua::State* L) { + if (luaL_newmetatable(L, HASH_CONTEXT_METATABLE)) { + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__gc"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "update"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "final"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "reset"); + lua::pushvalue(L, -1); + lua::setfield(L, "__index"); + } + lua::pop(L); + } + + int l_hash_new(lua::State* L) { + lua::check_argc(L, 1); + const auto name = lua::require_lstring(L, 1); + ensureHashContextMetatable(L); + void* memory = lua_newuserdata(L, sizeof(crypto::HashContext)); + new (memory) crypto::HashContext(name); + luaL_getmetatable(L, HASH_CONTEXT_METATABLE); + lua_setmetatable(L, -2); + return 1; + } + + int l_hmac(lua::State* L) { + lua::check_argc(L, 3); + return pushBytes( + L, + crypto::hmac( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3) + ) + ); + } + + int l_ed25519_verify(lua::State* L) { + lua::check_argc(L, 3); + const auto key = lua::require_lstring(L, 1); + const auto message = lua::require_lstring(L, 2); + const auto signature = lua::require_lstring(L, 3); + return verify(L, [=] { + return crypto::ed25519Verify(key, message, signature); + }); + } + + int l_ed25519_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::ed25519KeyPair()); + } + + int l_ed25519_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::ed25519Public(lua::require_lstring(L, 1))); + } + + int l_ed25519_sign(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::ed25519Sign( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_ecdsa_verify(lua::State* L) { + lua::check_argc(L, 5); + const auto curve = lua::require_lstring(L, 1); + const auto key = lua::require_lstring(L, 2); + const auto message = lua::require_lstring(L, 3); + const auto signature = lua::require_lstring(L, 4); + const auto hashName = lua::require_lstring(L, 5); + return verify(L, [=] { + return crypto::ecdsaVerify( + curve, key, message, signature, hashName + ); + }); + } + + int l_ecdsa_keypair(lua::State* L) { + lua::check_argc(L, 1); + return pushKeyPair(L, crypto::ecdsaKeyPair(lua::require_lstring(L, 1))); + } + + int l_ecdsa_public(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::ecdsaPublic( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_ecdsa_sign(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::ecdsaSign( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_rsa_pkcs1_verify(lua::State* L) { + lua::check_argc(L, 5); + const auto hashName = lua::require_lstring(L, 1); + const auto modulus = lua::require_lstring(L, 2); + const auto exponent = lua::require_lstring(L, 3); + const auto message = lua::require_lstring(L, 4); + const auto signature = lua::require_lstring(L, 5); + return verify(L, [=] { + return crypto::rsaPkcs1Verify( + hashName, modulus, exponent, message, signature + ); + }); + } + + int l_rsa_pss_verify(lua::State* L) { + lua::check_argc(L, 6); + const auto hashName = lua::require_lstring(L, 1); + const auto modulus = lua::require_lstring(L, 2); + const auto exponent = lua::require_lstring(L, 3); + const auto message = lua::require_lstring(L, 4); + const auto signature = lua::require_lstring(L, 5); + const int saltLength = requireInt(L, 6, "salt_length"); + return verify(L, [=] { + return crypto::rsaPssVerify( + hashName, modulus, exponent, message, signature, saltLength + ); + }); + } + + int l_x25519_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::x25519Public(lua::require_lstring(L, 1))); + } + + int l_x25519_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::x25519KeyPair()); + } + + int l_x25519(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::x25519( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_p256_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::p256Public(lua::require_lstring(L, 1))); + } + + int l_p256_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::ecdsaKeyPair("P-256")); + } + + int l_p256_shared(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::p256Shared( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_aes_gcm_encrypt(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::aesGcmEncrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_aes_gcm_decrypt(lua::State* L) { + lua::check_argc(L, 4); + const auto key = lua::require_lstring(L, 1); + const auto nonce = lua::require_lstring(L, 2); + const auto aad = lua::require_lstring(L, 3); + const auto ciphertext = lua::require_lstring(L, 4); + return decrypt(L, [=] { + return crypto::aesGcmDecrypt(key, nonce, aad, ciphertext); + }); + } + + int l_chacha20_poly1305_encrypt(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::chacha20Poly1305Encrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_chacha20_poly1305_decrypt(lua::State* L) { + lua::check_argc(L, 4); + const auto key = lua::require_lstring(L, 1); + const auto nonce = lua::require_lstring(L, 2); + const auto aad = lua::require_lstring(L, 3); + const auto ciphertext = lua::require_lstring(L, 4); + return decrypt(L, [=] { + return crypto::chacha20Poly1305Decrypt(key, nonce, aad, ciphertext); + }); + } + + int l_random_bytes(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::randomBytes(requireSize(L, 1, "length"))); + } + + int l_constant_time_equal(lua::State* L) { + lua::check_argc(L, 2); + return lua::pushboolean( + L, + crypto::constantTimeEqual( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_hkdf_extract(lua::State* L) { + lua::check_argc(L, 3); + return pushBytes( + L, + crypto::hkdfExtract( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3) + ) + ); + } + + int l_hkdf_expand(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::hkdfExpand( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + requireSize(L, 4, "length") + ) + ); + } + + int l_pbkdf2(lua::State* L) { + lua::check_argc(L, 5); + const auto iterations = requireSize(L, 4, "iterations"); + if (iterations == 0 || iterations > UINT32_MAX) { + throw std::runtime_error( + "iterations must be between 1 and 4294967295" + ); + } + return pushBytes( + L, + crypto::pbkdf2( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + static_cast(iterations), + requireSize(L, 5, "length") + ) + ); + } + + int l_scrypt(lua::State* L) { + const auto argc = lua::check_argc(L, 6, 7); + const auto maxMemory = + argc == 7 ? requireSize(L, 7, "max_memory") : std::size_t {0}; + return pushBytes( + L, + crypto::scrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + requireSize(L, 3, "n"), + requireSize(L, 4, "r"), + requireSize(L, 5, "p"), + requireSize(L, 6, "length"), + maxMemory + ) + ); + } + + void setFeature(lua::State* L, const char* name, bool available) { + lua::pushboolean(L, available); + lua::setfield(L, name); + } + + int l_features(lua::State* L) { + lua::check_argc(L, 0); + const auto features = crypto::features(); + lua::createtable(L, 0, 26); + lua::pushstring(L, "OpenSSL"); + lua::setfield(L, "backend"); + lua::pushstring(L, crypto::backendVersion()); + lua::setfield(L, "backend_version"); + lua::pushinteger(L, crypto::API_VERSION); + lua::setfield(L, "api_version"); + setFeature(L, "sha256", features.sha256); + setFeature(L, "sha384", features.sha384); + setFeature(L, "sha512", features.sha512); + setFeature(L, "md5", features.md5); + setFeature(L, "hmac", features.hmac); + setFeature(L, "streaming_hash", features.sha256); + setFeature(L, "ed25519", features.ed25519); + setFeature(L, "ed25519_verify", features.ed25519); + setFeature(L, "ecdsa", features.ecdsa); + setFeature(L, "ecdsa_verify", features.ecdsa); + setFeature(L, "rsa_pkcs1_verify", features.rsa); + setFeature(L, "rsa_pss_verify", features.rsa); + setFeature(L, "x25519", features.x25519); + setFeature(L, "p256", features.p256); + setFeature(L, "aes_128_gcm", features.aes128Gcm); + setFeature(L, "aes_256_gcm", features.aes256Gcm); + setFeature(L, "aes_gcm", features.aes128Gcm && features.aes256Gcm); + setFeature(L, "chacha20_poly1305", features.chacha20Poly1305); + setFeature(L, "random_bytes", features.random); + setFeature(L, "constant_time_equal", true); + setFeature(L, "hkdf", features.hkdf); + setFeature(L, "pbkdf2", features.pbkdf2); + setFeature(L, "scrypt", features.scrypt); + return 1; + } +} + +const luaL_Reg cryptolib[] = { + {"sha256", lua::wrap}, + {"sha384", lua::wrap}, + {"sha512", lua::wrap}, + {"md5", lua::wrap}, + {"hash", lua::wrap}, + {"hash_new", lua::wrap}, + {"hmac", lua::wrap}, + {"ed25519_keypair", lua::wrap}, + {"ed25519_public", lua::wrap}, + {"ed25519_sign", lua::wrap}, + {"ed25519_verify", lua::wrap}, + {"ecdsa_keypair", lua::wrap}, + {"ecdsa_public", lua::wrap}, + {"ecdsa_sign", lua::wrap}, + {"ecdsa_verify", lua::wrap}, + {"rsa_pkcs1_verify", lua::wrap}, + {"rsa_pss_verify", lua::wrap}, + {"x25519_keypair", lua::wrap}, + {"x25519_public", lua::wrap}, + {"x25519", lua::wrap}, + {"x25519_shared", lua::wrap}, + {"p256_keypair", lua::wrap}, + {"p256_public", lua::wrap}, + {"p256_shared", lua::wrap}, + {"aes_gcm_encrypt", lua::wrap}, + {"aes_gcm_decrypt", lua::wrap}, + {"chacha20_poly1305_encrypt", lua::wrap}, + {"chacha20_poly1305_decrypt", lua::wrap}, + {"random_bytes", lua::wrap}, + {"constant_time_equal", lua::wrap}, + {"hkdf_extract", lua::wrap}, + {"hkdf_expand", lua::wrap}, + {"pbkdf2", lua::wrap}, + {"scrypt", lua::wrap}, + {"features", lua::wrap}, + {NULL, NULL} +}; + +void initialize_cryptolib(lua::State* L) { + ensureHashContextMetatable(L); + if (lua::getglobal(L, "crypto")) { + lua::pushinteger(L, crypto::API_VERSION); + lua::setfield(L, "API_VERSION"); + lua::pop(L); + } +} diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 850b34109..383e7669b 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -53,6 +53,8 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "bjson", bjsonlib); openlib(L, "block", blocklib); openlib(L, "byteutil", byteutillib); + openlib(L, "crypto", cryptolib); + initialize_cryptolib(L); openlib(L, "file", filelib); openlib(L, "generation", generationlib); openlib(L, "item", itemlib); diff --git a/test/crypto/Crypto.cpp b/test/crypto/Crypto.cpp new file mode 100644 index 000000000..c4eb90fb3 --- /dev/null +++ b/test/crypto/Crypto.cpp @@ -0,0 +1,470 @@ +#include "crypto/Crypto.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + using PkeyHandle = std::unique_ptr; + using MdCtxHandle = std::unique_ptr; + + crypto::Bytes hex(const std::string& value) { + crypto::Bytes result; + result.reserve(value.size() / 2); + for (std::size_t i = 0; i < value.size(); i += 2) { + result.push_back( + static_cast( + std::stoul(value.substr(i, 2), nullptr, 16) + ) + ); + } + return result; + } + + std::string binary(const crypto::Bytes& value) { + return {reinterpret_cast(value.data()), value.size()}; + } + + crypto::Bytes sign( + EVP_PKEY* key, + const EVP_MD* digest, + std::string_view message, + int rsaPadding = 0, + int saltLength = 0 + ) { + MdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + EVP_PKEY_CTX* pkeyCtx = nullptr; + EXPECT_NE(ctx, nullptr); + EXPECT_EQ( + EVP_DigestSignInit(ctx.get(), &pkeyCtx, digest, nullptr, key), 1 + ); + if (rsaPadding != 0) { + EXPECT_GT(EVP_PKEY_CTX_set_rsa_padding(pkeyCtx, rsaPadding), 0); + if (rsaPadding == RSA_PKCS1_PSS_PADDING) { + EXPECT_GT( + EVP_PKEY_CTX_set_rsa_pss_saltlen(pkeyCtx, saltLength), 0 + ); + } + } + EXPECT_EQ( + EVP_DigestSignUpdate(ctx.get(), message.data(), message.size()), 1 + ); + std::size_t size = 0; + EXPECT_EQ(EVP_DigestSignFinal(ctx.get(), nullptr, &size), 1); + crypto::Bytes signature(size); + EXPECT_EQ(EVP_DigestSignFinal(ctx.get(), signature.data(), &size), 1); + signature.resize(size); + return signature; + } + + crypto::Bytes keyInteger(EVP_PKEY* key, const char* name) { + BIGNUM* raw = nullptr; + EXPECT_EQ(EVP_PKEY_get_bn_param(key, name, &raw), 1); + std::unique_ptr value(raw, BN_free); + crypto::Bytes result( + static_cast(BN_num_bytes(value.get())) + ); + BN_bn2bin(value.get(), result.data()); + return result; + } +} + +TEST(Crypto, HashesAndHmacMatchKnownVectors) { + EXPECT_EQ( + crypto::digest("SHA256", "abc"), + hex("ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad") + ); + EXPECT_EQ( + crypto::digest("SHA384", "abc"), + hex("cb00753f45a35e8bb5a03d699ac65007" + "272c32ab0eded1631a8b605a43ff5bed" + "8086072ba1e7cc2358baeca134c825a7") + ); + EXPECT_EQ( + crypto::digest("SHA512", "abc"), + hex("ddaf35a193617abacc417349ae204131" + "12e6fa4e89a97ea20a9eeee64b55d39" + "a2192992a274fc1a836ba3c23a3feebbd" + "454d4423643ce80e2a9ac94fa54ca49f") + ); + EXPECT_EQ( + crypto::digest("MD5", "abc"), hex("900150983cd24fb0d6963f7d28e17f72") + ); + const crypto::Bytes key(20, 0x0b); + EXPECT_EQ( + crypto::hmac("SHA256", binary(key), "Hi There"), + hex("b0344c61d8db38535ca8afceaf0bf12b" + "881dc200c9833da726e9376c2e32cff7") + ); +} + +TEST(Crypto, StreamingHashCanBeResetAndRejectsUpdateAfterFinal) { + crypto::HashContext context("SHA256"); + context.update("a"); + context.update(std::string("b\0c", 3)); + EXPECT_EQ( + context.final(), crypto::digest("SHA256", std::string("ab\0c", 4)) + ); + try { + context.update("more"); + FAIL() << "update after final must fail"; + } catch (const crypto::Error& error) { + EXPECT_EQ(error.code(), crypto::ErrorCode::InvalidState); + } + context.reset(); + context.update("abc"); + EXPECT_EQ(context.final(), crypto::digest("SHA256", "abc")); +} + +TEST(Crypto, Ed25519MatchesRfc8032Vector) { + auto publicKey = + hex("d75a980182b10ab7d54bfed3c964073a" + "0ee172f3daa62325af021a68f707511a"); + auto signature = + hex("e5564300c360ac729086e2cc806e828a" + "84877f1eb8e5d974d873e06522490155" + "5fb8821590a33bacc61e39701cf9b46b" + "d25bf5f0595bbe24655141438e7a100b"); + EXPECT_TRUE( + crypto::ed25519Verify(binary(publicKey), "", binary(signature)) + ); + signature[0] ^= 1; + EXPECT_FALSE( + crypto::ed25519Verify(binary(publicKey), "", binary(signature)) + ); +} + +TEST(Crypto, Ed25519GeneratesKeysAndSigns) { + const auto keyPair = crypto::ed25519KeyPair(); + ASSERT_EQ(keyPair.privateKey.size(), std::size_t {32}); + ASSERT_EQ(keyPair.publicKey.size(), std::size_t {32}); + EXPECT_EQ( + crypto::ed25519Public(binary(keyPair.privateKey)), keyPair.publicKey + ); + const auto signature = + crypto::ed25519Sign(binary(keyPair.privateKey), std::string("a\0b", 3)); + EXPECT_TRUE( + crypto::ed25519Verify( + binary(keyPair.publicKey), std::string("a\0b", 3), binary(signature) + ) + ); +} + +TEST(Crypto, EcdsaVerifiesAllSupportedCurves) { + struct Case { + const char* curve; + const char* opensslCurve; + const EVP_MD* digest; + const char* hash; + }; + const Case cases[] { + {"P-256", "prime256v1", EVP_sha256(), "SHA256"}, + {"P-384", "secp384r1", EVP_sha384(), "SHA384"}, + {"P-521", "secp521r1", EVP_sha512(), "SHA512"} + }; + for (const auto& item : cases) { + SCOPED_TRACE(item.curve); + PkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "EC", item.opensslCurve), + EVP_PKEY_free + ); + ASSERT_NE(key, nullptr); + std::size_t publicKeySize = 0; + ASSERT_EQ( + EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicKeySize + ), + 1 + ); + crypto::Bytes publicKey(publicKeySize); + ASSERT_EQ( + EVP_PKEY_get_octet_string_param( + key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + publicKey.data(), + publicKey.size(), + &publicKeySize + ), + 1 + ); + auto signature = sign(key.get(), item.digest, "message"); + EXPECT_TRUE( + crypto::ecdsaVerify( + item.curve, + binary(publicKey), + "message", + binary(signature), + item.hash + ) + ); + EXPECT_FALSE( + crypto::ecdsaVerify( + item.curve, + binary(publicKey), + "tampered", + binary(signature), + item.hash + ) + ); + } +} + +TEST(Crypto, EcdsaGeneratesKeysAndSignsOnAllSupportedCurves) { + struct Case { + const char* curve; + const char* hash; + std::size_t privateSize; + std::size_t publicSize; + }; + const Case cases[] { + {"P-256", "SHA256", 32, 65}, + {"P-384", "SHA384", 48, 97}, + {"P-521", "SHA512", 66, 133} + }; + for (const auto& item : cases) { + SCOPED_TRACE(item.curve); + const auto keyPair = crypto::ecdsaKeyPair(item.curve); + EXPECT_EQ(keyPair.privateKey.size(), item.privateSize); + EXPECT_EQ(keyPair.publicKey.size(), item.publicSize); + EXPECT_EQ( + crypto::ecdsaPublic(item.curve, binary(keyPair.privateKey)), + keyPair.publicKey + ); + const auto signature = crypto::ecdsaSign( + item.curve, binary(keyPair.privateKey), "message", item.hash + ); + EXPECT_TRUE( + crypto::ecdsaVerify( + item.curve, + binary(keyPair.publicKey), + "message", + binary(signature), + item.hash + ) + ); + } +} + +TEST(Crypto, RsaVerifiesPkcs1AndPssSignatures) { + PkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "RSA", 2048), EVP_PKEY_free + ); + ASSERT_NE(key, nullptr); + const auto modulus = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_N); + const auto exponent = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_E); + + auto pkcs1 = sign(key.get(), EVP_sha256(), "message", RSA_PKCS1_PADDING); + EXPECT_TRUE( + crypto::rsaPkcs1Verify( + "SHA256", + binary(modulus), + binary(exponent), + "message", + binary(pkcs1) + ) + ); + EXPECT_FALSE( + crypto::rsaPkcs1Verify( + "SHA256", + binary(modulus), + binary(exponent), + "tampered", + binary(pkcs1) + ) + ); + + auto pss = sign( + key.get(), + EVP_sha256(), + "message", + RSA_PKCS1_PSS_PADDING, + RSA_PSS_SALTLEN_DIGEST + ); + EXPECT_TRUE( + crypto::rsaPssVerify( + "SHA256", + binary(modulus), + binary(exponent), + "message", + binary(pss), + -1 + ) + ); + EXPECT_FALSE( + crypto::rsaPssVerify( + "SHA256", + binary(modulus), + binary(exponent), + "tampered", + binary(pss), + -1 + ) + ); +} + +TEST(Crypto, X25519MatchesRfc7748PublicKeyVector) { + const auto privateKey = + hex("77076d0a7318a57d3c16c17251b26645" + "df4c2f87ebc0992ab177fba51db92c2a"); + EXPECT_EQ( + crypto::x25519Public(binary(privateKey)), + hex("8520f0098930a754748b7ddcb43ef75a0" + "dbf3a0d26381af4eba4a98eaa9b4e6a") + ); +} + +TEST(Crypto, X25519GeneratedKeyPairsDeriveTheSameSecret) { + const auto alice = crypto::x25519KeyPair(); + const auto bob = crypto::x25519KeyPair(); + EXPECT_EQ(alice.privateKey.size(), std::size_t {32}); + EXPECT_EQ(alice.publicKey.size(), std::size_t {32}); + EXPECT_EQ(crypto::x25519Public(binary(alice.privateKey)), alice.publicKey); + EXPECT_EQ( + crypto::x25519(binary(alice.privateKey), binary(bob.publicKey)), + crypto::x25519(binary(bob.privateKey), binary(alice.publicKey)) + ); +} + +TEST(Crypto, P256DerivesPublicKeyAndSharedSecret) { + crypto::Bytes privateA(32, 0); + crypto::Bytes privateB(32, 0); + privateA.back() = 1; + privateB.back() = 2; + const auto publicA = crypto::p256Public(binary(privateA)); + const auto publicB = crypto::p256Public(binary(privateB)); + EXPECT_EQ( + publicA, + hex("046b17d1f2e12c4247f8bce6e563a440" + "f277037d812deb33a0f4a13945d898c296" + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e16" + "2bce33576b315ececbb6406837bf51f5") + ); + EXPECT_EQ( + crypto::p256Shared(binary(privateA), binary(publicB)), + crypto::p256Shared(binary(privateB), binary(publicA)) + ); +} + +TEST(Crypto, AesGcmMatchesKnownVectorAndRejectsTampering) { + const crypto::Bytes key(16, 0); + const crypto::Bytes nonce(12, 0); + const crypto::Bytes plaintext(16, 0); + auto ciphertext = crypto::aesGcmEncrypt( + binary(key), binary(nonce), "", binary(plaintext) + ); + EXPECT_EQ( + ciphertext, + hex("0388dace60b6a392f328c2b971b2fe78" + "ab6e47d42cec13bdf53a67b21257bddf") + ); + EXPECT_EQ( + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ), + plaintext + ); + ciphertext.back() ^= 1; + EXPECT_THROW( + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ), + std::runtime_error + ); +} + +TEST(Crypto, Chacha20Poly1305RoundTripsAndRejectsTampering) { + const crypto::Bytes key(32, 0); + const crypto::Bytes nonce(12, 0); + auto ciphertext = crypto::chacha20Poly1305Encrypt( + binary(key), binary(nonce), "aad", "plaintext" + ); + EXPECT_EQ( + binary( + crypto::chacha20Poly1305Decrypt( + binary(key), binary(nonce), "aad", binary(ciphertext) + ) + ), + "plaintext" + ); + ciphertext.back() ^= 1; + EXPECT_THROW( + crypto::chacha20Poly1305Decrypt( + binary(key), binary(nonce), "aad", binary(ciphertext) + ), + std::runtime_error + ); +} + +TEST(Crypto, HkdfMatchesRfc5869Vector) { + const crypto::Bytes ikm(22, 0x0b); + const auto salt = hex("000102030405060708090a0b0c"); + const auto info = hex("f0f1f2f3f4f5f6f7f8f9"); + const auto prk = crypto::hkdfExtract("SHA256", binary(salt), binary(ikm)); + EXPECT_EQ( + prk, + hex("077709362c2e32df0ddc3f0dc47bba63" + "90b6c73bb50f9c3122ec844ad7c2b3e5") + ); + EXPECT_EQ( + crypto::hkdfExpand("SHA256", binary(prk), binary(info), 42), + hex("3cb25f25faacd57a90434f64d0362f2a" + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + "34007208d5b887185865") + ); +} + +TEST(Crypto, PasswordKdfsMatchKnownVectors) { + EXPECT_EQ( + crypto::pbkdf2("SHA256", "password", "salt", 1, 32), + hex("120fb6cffcf8b32c43e7225256c4f837" + "a86548c92ccc35480805987cb70be17b") + ); + EXPECT_EQ( + crypto::scrypt("", "", 16, 1, 1, 64), + hex("77d6576238657b203b19ca42c18a0497" + "f16b4844e3074ae8dfdffa3fede21442f" + "cd0069ded0948f8326a753a0fc81f17e" + "8d3e0fb2e0d3628cf35e20c38d18906") + ); +} + +TEST(Crypto, ReportsRuntimeCapabilitiesAndStableErrors) { + const auto available = crypto::features(); + EXPECT_TRUE(available.sha256); + EXPECT_TRUE(available.ed25519); + EXPECT_TRUE(available.ecdsa); + EXPECT_TRUE(available.x25519); + EXPECT_TRUE(available.aes128Gcm); + EXPECT_TRUE(available.pbkdf2); + EXPECT_TRUE(available.scrypt); + EXPECT_EQ(crypto::API_VERSION, 1); + + const crypto::Bytes key(16, 0); + const crypto::Bytes nonce(12, 0); + auto ciphertext = + crypto::aesGcmEncrypt(binary(key), binary(nonce), "", "secret"); + ciphertext.back() ^= 1; + try { + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ); + FAIL() << "tampered ciphertext must fail"; + } catch (const crypto::Error& error) { + EXPECT_EQ(error.code(), crypto::ErrorCode::AuthenticationFailed); + EXPECT_STREQ(error.codeName(), "authentication_failed"); + } +} + +TEST(Crypto, UtilitiesHandleBinaryData) { + const std::string binaryValue("a\0b", 3); + EXPECT_TRUE(crypto::constantTimeEqual(binaryValue, binaryValue)); + EXPECT_FALSE(crypto::constantTimeEqual(binaryValue, "a")); + EXPECT_FALSE(crypto::constantTimeEqual("left", "lest")); + EXPECT_EQ(crypto::randomBytes(32).size(), std::size_t {32}); +} diff --git a/test/crypto/CryptoLua.cpp b/test/crypto/CryptoLua.cpp new file mode 100644 index 000000000..3bce6af04 --- /dev/null +++ b/test/crypto/CryptoLua.cpp @@ -0,0 +1,70 @@ +#include + +#include + +#include "logic/scripting/lua/libs/api_lua.hpp" + +namespace { + struct LuaCloser { + void operator()(lua::State* state) const { + lua_close(state); + } + }; +} + +TEST(CryptoLua, PublicV1ApiWorksFromLua) { + std::unique_ptr state(luaL_newstate()); + ASSERT_NE(state, nullptr); + luaL_openlibs(state.get()); + lua::openlib(state.get(), "crypto", cryptolib); + initialize_cryptolib(state.get()); + + const char* script = R"lua( + assert(crypto.API_VERSION == 1) + local features = crypto.features() + assert(features.api_version == 1) + assert(features.backend == "OpenSSL") + + local binary = "a\0b" + assert(crypto.hash("SHA256", binary) == crypto.sha256(binary)) + local stream = crypto.hash_new("SHA256") + assert(stream:update("a"):update("\0b"):final() == crypto.sha256(binary)) + stream:reset():update("abc") + assert(stream:final() == crypto.sha256("abc")) + + local private, public = crypto.ed25519_keypair() + assert(crypto.ed25519_public(private) == public) + local signature = crypto.ed25519_sign(private, binary) + assert(crypto.ed25519_verify(public, binary, signature) == true) + local ok, error_code = crypto.ed25519_verify(public, "changed", signature) + assert(ok == false and error_code == "invalid_signature") + + local x_private_a, x_public_a = crypto.x25519_keypair() + local x_private_b, x_public_b = crypto.x25519_keypair() + assert(crypto.x25519(x_private_a, x_public_b) == + crypto.x25519_shared(x_private_b, x_public_a)) + + local p_private_a, p_public_a = crypto.p256_keypair() + local p_private_b, p_public_b = crypto.p256_keypair() + assert(crypto.p256_public(p_private_a) == p_public_a) + assert(crypto.p256_shared(p_private_a, p_public_b) == + crypto.p256_shared(p_private_b, p_public_a)) + + local key = crypto.random_bytes(16) + local nonce = crypto.random_bytes(12) + local ciphertext = crypto.aes_gcm_encrypt(key, nonce, "aad", binary) + assert(crypto.aes_gcm_decrypt(key, nonce, "aad", ciphertext) == binary) + local plaintext, decrypt_error = crypto.aes_gcm_decrypt( + key, nonce, "changed", ciphertext + ) + assert(plaintext == nil and decrypt_error == "authentication_failed") + + assert(#crypto.pbkdf2("SHA256", "password", "salt", 1, 32) == 32) + assert(#crypto.scrypt("password", "salt", 16, 1, 1, 32) == 32) + )lua"; + + const int result = luaL_dostring(state.get(), script); + if (result != 0) { + FAIL() << lua_tostring(state.get(), -1); + } +} diff --git a/vcpkg.json b/vcpkg.json index 4f56dc6c2..79f2acebe 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -14,6 +14,7 @@ "entt", "gtest", "curl", - "freetype" + "freetype", + "openssl" ] }