diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..5bfebeb4d 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,13 +46,13 @@ 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 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --memchecker valgrind - name: Build AppImage uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 env: 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/.github/workflows/macos.yml b/.github/workflows/macos.yml index c3f5156f9..b4d9f6328 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -23,13 +23,13 @@ jobs: - name: Install dependencies from brew run: | - brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype + brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype - name: Configure run: | cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \ -DOPENAL_LIBRARY=$(brew --prefix openal-soft)/lib/libopenal.dylib \ - -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" + -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" - name: Build run: | @@ -45,11 +45,11 @@ jobs: run: ctest --output-on-failure --test-dir build - name: Run engine tests - timeout-minutes: 1 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always --debugger "lldb -batch -o 'run' -o 'thread backtrace all' -o 'quit' -- " - name: Create DMG run: | mkdir VoxelEngineDmgContent diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f248b6ab6..ffcb0c318 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -38,7 +38,7 @@ jobs: - name: Run engine tests run: | build/vctest/RelWithDebInfo/vctest.exe -e build/RelWithDebInfo/VoxelEngine.exe -d dev/tests -u build - timeout-minutes: 1 + timeout-minutes: 5 - name: Package for Windows run: | mkdir packaged 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/dev/tests/crypto.lua b/dev/tests/crypto.lua new file mode 100644 index 000000000..96b36ecd9 --- /dev/null +++ b/dev/tests/crypto.lua @@ -0,0 +1,41 @@ +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) diff --git a/dev/tests/network_http.lua b/dev/tests/network_http.lua index 5861460de..12149de09 100644 --- a/dev/tests/network_http.lua +++ b/dev/tests/network_http.lua @@ -8,4 +8,4 @@ end, function (code) response_received = true end) -app.sleep_until(function () return response_received end, nil, 10) +app.sleep_until(function () return response_received end, nil, 5) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index e25c6d71d..c2c852c35 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -127,6 +127,11 @@ Defines the entity's material (same as for blocks). The default value is the same as the entity name. Determines which skeleton will be used by the entity. See [rigging](rigging.md). +### *lighting-mode* + +- *bone-based* - lighting is calculated for each skeletal bone separately. +- *solid* - lighting is calculated for the entire entity. + ## Saving/Loading In addition to custom components, the engine automatically saves data from the built-in ones: transform, rigidbody, skeleton. 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..b3106d104 --- /dev/null +++ b/doc/en/scripting/builtins/libcrypto.md @@ -0,0 +1,144 @@ +# *crypto* library + +The library provides common cryptographic functions + +## 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 needed for compatibility with old formats. SHA256 is better for new data + +Large files can be passed in parts + +```lua +local hash = crypto.hash_new("SHA256") + +hash:update(part1) +hash:update(part2) + +local result = hash:final() +``` + +The context can be cleared with `reset` after `final` + +## 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 +``` + +Ed25519 keys are 32 bytes long. A signature is 64 bytes long + +```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 +``` + +Available curves are `P-256`, `P-384` and `P-521` + +An ECDSA public key is stored as an uncompressed SEC1 point. A signature is +stored in DER format + +```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 modulus and exponent are passed in big endian. A `salt_length` value of `-1` +uses the hash size + +## 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` perform the same operation + +The shared secret should not be used as a ready key. Pass it through HKDF first + +## 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 encrypted data + +Never reuse a nonce 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 passwords + +`features` returns the OpenSSL version and a list of available features + +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/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index a7ddbeaec..3dc10612b 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -61,6 +61,12 @@ gui.set_active_frame( -- Used for custom projection (e.g., in 3D) [optional] cursorLocator: function() -> number, number ) + +-- Creates a screenshot of a frame as a Canvas object if the frame ID is specified, or the entire window if nil. +gui.screenshot( + -- ID of the frame created via gui.create_frame + [optional] frameId: str +) -> Canvas | nil ``` ## Markup diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index 0ce5c1b0e..8893407d4 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ A library for working with the network. ## HTTP requests +There is a configurable function `network.request` that allows performing HTTP requests with various methods (GET, POST, PUT, DELETE, etc.) and configuring headers, request body, timeout, and other parameters. + +> To pass binary data in the request body, use a byte array (Bytearray) or a string. In `on_response`, the `body` string can be converted to a byte array using `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Request method (GET, POST, PUT, DELETE, etc.) + method: string, + -- Request body as a string or Bytearray + body: table|string, + -- List of additional request headers + headers: table, + -- Timeout in milliseconds + timeout: int, + -- Whether to verify the SSL certificate + verify_ssl: boolean, + -- Function called when a response is received + on_response: function(response: { + -- HTTP response status code + status: int, + -- Response body as a string + body: string, + -- List of response headers + headers: table + }), + } +) +``` + +### Simplified functions for GET and POST requests + ```lua -- Performs a GET request to the specified URL. network.get( diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index d9194ea93..0473fc852 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -190,10 +190,14 @@ rig:is_visible([optional] index: int) -> bool rig:set_visible([optional] index: int, status: bool) -- Returns the color of the entity -rig:get_color() -> vec3 +rig:get_color() -> vec4 +-- Return the color of the bone by index +rig:get_color(index: int) -> vec4 -- Sets the color of the entity -rig:set_color(color: vec3) +rig:set_color(color: vec3 | vec4) +-- Sets the color of the bone by index +rig:set_color(color: vec3 | vec4, index: int) ``` > [!WARNING] diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index 71dc6dc88..20ff752c7 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -127,6 +127,11 @@ Значение по-умолчанию совпадает с именем сущности. Определяет то, какой скелет будет использоваться сущностью. См. [риггинг](rigging.md). +### Тип освещения - *lighting-mode* + +- *bone-based* - освещение рассчитывается для каждой кости скелета отдельно. +- *solid* - освещение рассчитывается для всей сущности в целом. + ## Сохранение/загрузка Кроме данных пользовательских компонентов, движок автоматически сохраняет данные встроенных: transform, rigidbody, skeleton. diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 1959c4649..377b2c6da 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -16,6 +16,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..50112710b --- /dev/null +++ b/doc/ru/scripting/builtins/libcrypto.md @@ -0,0 +1,146 @@ +# Библиотека *crypto* + +Библиотека предоставляет основные криптографические функции + +## Хеширование + +```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` +использует размер хеша + +## Обмен ключами + +```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/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index ea1221415..51ac7307f 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -69,6 +69,12 @@ gui.set_active_frame( -- Используется для пользовательской проекции (например в 3D) [опционально] cursorLocator: function() -> number, number ) + +-- Создаёт снимок фрейма в виде объекта Canvas если указан id фрейма, или всего окна, в случае nil. +gui.screenshot( + -- id фрейма, созданного через gui.create_frame + [опционально] frameId: str +) -> Canvas | nil ``` ## Разметка diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 98c4e51e0..c60f0f178 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ ## HTTP-Запросы +Существует настраиваемая функция `network.request`, которая позволяет выполнять HTTP-запросы с различными методами (GET, POST, PUT, DELETE и т.д.) и настраивать заголовки, тело запроса, таймаут и другие параметры. + +> Для передачи двоичных данных в теле запроса, используйте массив байт (Bytearray) или строку. В `on_response` строку `body` можно преобразовать в массив байт с помощью `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Метод запроса (GET, POST, PUT, DELETE и т.д.) + method: string, + -- Тело запроса в виде строки или массива байт (Bytearray) + body: table|string, + -- Список дополнительных заголовков запроса + headers: table, + -- Таймаут в миллисекундах + timeout: int, + -- Проверять ли SSL-сертификат + verify_ssl: boolean, + -- Функция, вызываемая при получении ответа + on_response: function(response: { + -- HTTP-код ответа + status: int, + -- Тело ответа в виде строки + body: string, + -- Список заголовков ответа + headers: table + }), + } +) +``` + +### Упрощённые функции для GET и POST запросов + ```lua -- Выполняет GET запрос к указанному URL. network.get( @@ -231,4 +264,4 @@ network.get_total_download() -> int ```lua -- Ищет свободный для использования порт. network.find_free_port() -> int | nil -``` \ No newline at end of file +``` diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index fa6186d41..d6a823cf0 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -179,10 +179,14 @@ rig:is_visible([optional] index: int) -> bool rig:set_visible([optional] index: int, status: bool) -- Возвращает цвет сущности -rig:get_color() -> vec3 +rig:get_color() -> vec4 +-- Возвращает цвет кости по индексу +rig:get_color(index: int) -> vec4 -- Устанавливает цвет сущности -rig:set_color(color: vec3) +rig:set_color(color: vec3 | vec4) +-- Устанавливает цвет кости по индексу +rig:set_color(color: vec3 | vec4, index: int) ``` ## Пользовательские компоненты diff --git a/doc/ru/vca.md b/doc/ru/vca.md index eb1f77b07..795541dc1 100644 --- a/doc/ru/vca.md +++ b/doc/ru/vca.md @@ -38,6 +38,18 @@ VCA файл состоит из набора директив, порядок Мета-информация применяется глобально. Поведение не зависит от её положения в файле, но рекомендуется располагать её в начале. +## Дополнительно + +- `curve` - объявляет пользовательский тип кривой. Указывается имя кривой `name` и выражение вычисления значения `func`. + +В выражении доступны два ключа `kl`, `kr` и значение `t` в диапазоне [0..1]. + +Пример: + +`@curve test-linear func (kl.value + (kr.value - kl.value) * t)` + +Пользовательские кривые указываются с префиксом `.`. Пример: `@move by y curve .test-linear {...}`. + ## Кривые с ключевыми кадрами Требуется через атрибут `curve` указать тип кривой: 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/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 302f60644..268875874 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -208,7 +208,7 @@ function Version.__less(ver1, ver2) end function Version.__greater_or_equal(ver1, ver2) - return not Version.__less(ver2, ver1) + return not Version.__less(ver1, ver2) end function Version.__less_or_equal(ver1, ver2) @@ -227,6 +227,11 @@ function Version.compare(op, ver1, ver2) ver1 = string.split(ver1, ".") ver2 = string.split(ver2, ".") + for i = 1, 3 do + ver1[i] = tonumber(ver1[i]) or 0 + ver2[i] = tonumber(ver2[i]) or 0 + end + local comparison_func = Version.operators[op]; if comparison_func then @@ -252,7 +257,7 @@ end local function compare_version(op, dependent_version, actual_version) if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then - return Version.compare(op, dep_ver, actual_version) + return Version.compare(op, actual_version, dependent_version) elseif dependent_version == "*" or dependent_version == actual_version then return true else diff --git a/res/modules/animation.lua b/res/modules/animation.lua index a54a090b1..3f6acf351 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -1,21 +1,5 @@ local internals = __vc_internals -local this = { - CH_TRANSLATE = 1, - CH_ROTATE = 2, - CH_SCALE = 3, - CH_ZOOM = 4, - - INT_CONST = 1, - INT_LINEAR = 2, - INT_BEZIER = 3, - - TRACE_CODEGEN = false, -} - -local INT_CONST = this.INT_CONST -local INT_BEZIER = this.INT_BEZIER - local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -53,264 +37,23 @@ local function bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end -local patterns = { - {name="sint", pattern="sin(t)"}, - {name="sint2", pattern="sin(t * 2)"}, -} -local exclude_patters = { - "end", - (string.pattern_safe("'")), - (string.pattern_safe('"')), - (string.pattern_safe("--")), - (string.pattern_safe("..")), -} +local this = { + CH_TRANSLATE = 1, + CH_ROTATE = 2, + CH_SCALE = 3, + CH_ZOOM = 4, --- TODO: replace with actual expression -> lua translator -local function process_expression(src, memoised) - for i, pattern in ipairs(exclude_patters) do - if src:find(pattern) then - debug.print(exclude_patters) - error("invalid syntax "..string.escape(src)) - end - end - for i, pattern in ipairs(patterns) do - local pattern_safe = string.pattern_safe(pattern.pattern) - if src:find(pattern_safe) then - memoised[pattern.name] = pattern.pattern - src = src:gsub(pattern_safe, pattern.name) - end - end - return src -end + INT_CONST = 1, + INT_LINEAR = 2, + INT_BEZIER = 3, + INT_CUSTOM = 4, -local function key_neighbors(keys, frame) - local left = 1 - local right = #keys + TRACE_CODEGEN = false, - while left <= right do - local mid = math.floor((left + right) / 2) - - if keys[mid].frame < frame then - left = mid + 1 - elseif keys[mid].frame > frame then - right = mid - 1 - else - return mid, mid - end - end - if left > #keys then - left = #keys - end - return right, left -end - -local env = { - mat4 = mat4, - e = math.exp(1), - X = {1, 0, 0}, - Y = {0, 1, 0}, - Z = {0, 0, 1}, - DST = mat4.idt(), - value_at = function(keys, frame, interp) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - if interp == INT_CONST then - return left.value - end - right = keys[right] - if left == nil then - return right.value - end - local t = (frame - left.frame) / (right.frame - left.frame) - if interp == INT_BEZIER then - return bezier_interpolation(left, right, t) - end - return left.value * (1.0 - t) + right.value * t - end, - set_matrix = function(target, matrix) - local info = mat4.decompose(matrix) - if info then - local pos = info.translation - local rot = info.rotation - local scale = info.scale - if target.set_pos then - target:set_pos(pos) - end - if target.set_rot then - target:set_rot(rot) - end - if target.set_scale then - target:set_scale(scale) - end - end - end, - dump = debug.print -} - -local math_funcs = { - "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", - "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" -} -for _, name in ipairs(math_funcs) do - env[name] = math[name] -end - -local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) - local lines = lineset.lines - local code = "" - local has_tsf = false - local translation = {false, false, false} - local rotation = {false, false, false} - local scale = {false, false, false} - for i, line in ipairs(lines) do - if line.expression then - code = code .. "\n local l" .. i .. " = (" .. - process_expression(line.expression, memoised) .. ")" - elseif line.keys then - local target_keysets = keysets[lineset.target_name] - if not target_keysets then - target_keysets = {} - keysets[lineset.target_name] = target_keysets - end - target_keysets[i] = line.keys - code = code .. string.format( - "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, line.interp) - end - - if line.channel == this.CH_TRANSLATE then - translation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ROTATE then - rotation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_SCALE then - scale[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ZOOM then - code = code .. "\n zoom = l" .. i - end - end - - if not has_tsf or not use_tsf then - return code - end - - code = code .. "\n mat4.idt(dst)" - if translation[1] or translation[2] or translation[3] then - code = code .. "\n mat4.translate(dst, {" .. - (translation[1] and ("l" .. translation[1]) or '0').. ", " .. - (translation[2] and ("l" .. translation[2]) or '0').. ", " .. - (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" - end - - local axis_names = {"X", "Y", "Z"} - local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order - for i=1,3 do - local axis = axis_indices[rotation_order[i]] - local var = rotation[axis] - if var then - code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ", l" .. var .. ", dst)" - end - end - - if scale[1] or scale[2] or scale[3] then - code = code .. "\n mat4.scale(dst, {" .. - (scale[1] and ("l" .. scale[1]) or '1').. ", " .. - (scale[2] and ("l" .. scale[2]) or '1').. ", " .. - (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" - end - - return code -end - -local function codegen_rig_target(raw_track, memoised, keysets) - local code = "\n if target.set_matrix and target.index then\n" - code = code .. " local dst = DST\n" - for bone, lineset in pairs(raw_track.linesets) do - if lineset.target_type ~= "bone" then - goto continue - end - local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) - - code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" - ::continue:: - end - return code .. " end" -end - -local function codegen_object_target(raw_track, memoised, keysets) - local code = "\n if target.set_pos then\n" - code = code .. " local dst = DST\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " set_matrix(target, dst)\n" - return code .. " end" -end - -local function codegen_camera_target(raw_track, memoised, keysets) - local code = "\n if target.set_zoom then\n" - code = code .. " local zoom = 1.0\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, false) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " target:set_zoom(zoom)\n" - return code .. " end" -end - -function this.compile_track(raw_track, track_name) - local code = "" - local memoised = {} - local keysets = {} - - code = code .. codegen_rig_target(raw_track, memoised, keysets) - code = code .. codegen_object_target(raw_track, memoised, keysets) - code = code .. codegen_camera_target(raw_track, memoised, keysets) - - local memoised_code = "" - for name, expression in pairs(memoised) do - memoised_code = memoised_code .. "\n local " .. name .. " = " - .. expression - end - - if #memoised_code > 0 then - code = memoised_code .. "\n" .. code - end - - local src = "return function(target, t, m)\n" - .. code .. "\nend" - - if this.TRACE_CODEGEN then - debug.log("[".. - string.escape(track_name or "nil").." codegen trace]:\n"..src) - end - - local generator, err = load( - src, "", "bt", table.extend({keysets = keysets}, env)) - if not generator then - error(err) - end - return { - duration = raw_track.duration, - func = generator(), + maths = { + bezier_interpolation = bezier_interpolation } -end +} local loaded_tracks = {} local backup_tracks = {} @@ -330,8 +73,35 @@ function this.action(func) table.insert(running_actions, coroutine.create(func)) end +local PlayingTrack = { + __index = { + stop = function(self) + table.remove_value(playing_tracks, self) + self.__timer = 0.0 + self.__playing = false + end, + pause = function(self) + table.remove_value(playing_tracks, self) + self.__playing = false + end, + resume = function(self) + if not self.__playing then + table.insert(playing_tracks, self) + self.__playing = true + end + end + } +} + function this.play(name, target) - table.insert(playing_tracks, {name=name, target=target, timer=0.0}) + local track = setmetatable({ + name = name, + target = target, + __timer = 0.0, + __playing = true, + }, PlayingTrack) + table.insert(playing_tracks, track) + return track end function internals.on_animation_frame() @@ -353,11 +123,11 @@ function internals.on_animation_frame() debug.error("animation track not found: "..track_info.name) table.remove(playing_tracks, i) else - track_info.timer = track_info.timer + delta - if track_info.timer > track.duration then + track_info.__timer = track_info.__timer + delta + if track_info.__timer > track.duration then table.remove(playing_tracks, i) else - track.func(track_info.target, track_info.timer) + track.func(track_info.target, track_info.__timer) end end end diff --git a/res/modules/internal/animation_codegen.lua b/res/modules/internal/animation_codegen.lua new file mode 100644 index 000000000..0b2cd7ce3 --- /dev/null +++ b/res/modules/internal/animation_codegen.lua @@ -0,0 +1,298 @@ +local internals = __vc_internals + +local INT_CONST = animation.INT_CONST +local INT_BEZIER = animation.INT_BEZIER + +local bezier_interpolation = animation.maths.bezier_interpolation + +local patterns = {} +local exclude_patters = { + "end", + (string.pattern_safe("'")), + (string.pattern_safe('"')), + (string.pattern_safe("--")), + (string.pattern_safe("..")), +} + +-- TODO: replace with actual expression -> lua translator +local function process_expression(src, memoised) + for i, pattern in ipairs(exclude_patters) do + if src:find(pattern) then + debug.print(exclude_patters) + error("invalid syntax "..string.escape(src)) + end + end + for i, pattern in ipairs(patterns) do + local pattern_safe = string.pattern_safe(pattern.pattern) + if src:find(pattern_safe) then + memoised[pattern.name] = pattern.pattern + src = src:gsub(pattern_safe, pattern.name) + end + end + return src +end + +local function key_neighbors(keys, frame) + local left = 1 + local right = #keys + + while left <= right do + local mid = math.floor((left + right) / 2) + + if keys[mid].frame < frame then + left = mid + 1 + elseif keys[mid].frame > frame then + right = mid - 1 + else + return mid, mid + end + end + if left > #keys then + left = #keys + end + return right, left +end + +local env = { + mat4 = mat4, + e = math.exp(1), + X = {1, 0, 0}, + Y = {0, 1, 0}, + Z = {0, 0, 1}, + DST = mat4.idt(), + value_at = function(keys, frame, interp) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + if interp == INT_CONST then + return left.value + end + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + if interp == INT_BEZIER then + return bezier_interpolation(left, right, t) + elseif type(interp) == "function" then + return interp(left.value, right.value, t) + end + return left.value * (1.0 - t) + right.value * t + end, + value_at_custom = function(keys, frame, func) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + return func(left, right, t) + end, + set_matrix = function(target, matrix) + local info = mat4.decompose(matrix) + if info then + local pos = info.translation + local rot = info.rotation + local scale = info.scale + if target.set_pos then + target:set_pos(pos) + end + if target.set_rot then + target:set_rot(rot) + end + if target.set_scale then + target:set_scale(scale) + end + end + end, + dump = debug.print +} + +local math_funcs = { + "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", + "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" +} +for _, name in ipairs(math_funcs) do + env[name] = math[name] +end + +local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) + local lines = lineset.lines + local code = "" + local has_tsf = false + local translation = {false, false, false} + local rotation = {false, false, false} + local scale = {false, false, false} + for i, line in ipairs(lines) do + if line.expression then + code = code .. "\n local l" .. i .. " = (" .. + process_expression(line.expression, memoised) .. ")" + elseif line.keys then + local target_keysets = keysets[lineset.target_name] + if not target_keysets then + target_keysets = {} + keysets[lineset.target_name] = target_keysets + end + target_keysets[i] = line.keys + + if line.curve_func then + local valueat = string.format("curves[%s]", string.escape(line.curve_func)) + + code = code .. string.format( + "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, valueat) + else + code = code .. string.format( + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) + end + end + + if line.channel == animation.CH_TRANSLATE then + translation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ROTATE then + rotation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_SCALE then + scale[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ZOOM then + code = code .. "\n zoom = l" .. i + end + end + + if not has_tsf or not use_tsf then + return code + end + + code = code .. "\n mat4.idt(dst)" + if translation[1] or translation[2] or translation[3] then + code = code .. "\n mat4.translate(dst, {" .. + (translation[1] and ("l" .. translation[1]) or '0').. ", " .. + (translation[2] and ("l" .. translation[2]) or '0').. ", " .. + (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" + end + + local axis_names = {"X", "Y", "Z"} + local axis_indices = {X=1, Y=2, Z=3} + local rotation_order = raw_track.rotation_order or "XYZ" + for i=1,3 do + local axis = axis_indices[rotation_order[i]] + local var = rotation[axis] + if var then + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. + ", l" .. var .. ", dst)" + end + end + + if scale[1] or scale[2] or scale[3] then + code = code .. "\n mat4.scale(dst, {" .. + (scale[1] and ("l" .. scale[1]) or '1').. ", " .. + (scale[2] and ("l" .. scale[2]) or '1').. ", " .. + (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" + end + + return code +end + +local function codegen_rig_target(raw_track, context) + local code = "\n if target.set_matrix and target.index then\n" + code = code .. " local dst = DST\n" + for bone, lineset in pairs(raw_track.linesets) do + if lineset.target_type ~= "bone" then + goto continue + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" + ::continue:: + end + return code .. " end" +end + +local function codegen_object_target(raw_track, context) + local code = "\n if target.set_pos then\n" + code = code .. " local dst = DST\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " set_matrix(target, dst)\n" + return code .. " end" +end + +local function codegen_camera_target(raw_track, context) + local code = "\n if target.set_zoom then\n" + code = code .. " local zoom = 1.0\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, false) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " target:set_zoom(zoom)\n" + return code .. " end" +end + + +function internals.compile_animation_track(raw_track, track_name) + local code = "" + local context = { + memoised = {}, + keysets = {}, + curves = {}, + } + for name, curve in pairs(raw_track.curves) do + context.curves[name] = load(string.format( + "return function(kl, kr, t) return %s end", + process_expression(curve.func, context.memoised) + ), "", "t", env)() + end + + code = code .. codegen_rig_target(raw_track, context) + code = code .. codegen_object_target(raw_track, context) + code = code .. codegen_camera_target(raw_track, context) + + local memoised_code = "" + for name, expression in pairs(context.memoised) do + memoised_code = memoised_code .. "\n local " .. name .. " = " + .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(target, t, m)\n" + .. code .. "\nend" + + if animation.TRACE_CODEGEN then + debug.log("[".. + string.escape(track_name or "nil").." codegen trace]:\n"..src) + end + + local generator, err = load( + src, "", "bt", table.extend({ + keysets = context.keysets, curves = context.curves + }, env)) + if not generator then + error(err) + end + return { + duration = raw_track.duration, + func = generator(), + } +end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 0faa89404..f06c99a7b 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -1,7 +1,6 @@ local internals = __vc_internals local DEFAULT_FPS = 60 -local INT_BEZIER = animation.INT_BEZIER local action_to_channel = { move = animation.CH_TRANSLATE, @@ -25,18 +24,23 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" + raw_track.rotation_order = string.upper(node["rotation-order"] or "XYZ") end local function parse_curve(line, node) - line.interp = curve_to_interp[node.curve] + if node.curve:starts_with(".") then + line.interp = animation.INT_CUSTOM + line.curve_func = node.curve:sub(2) + else + line.interp = curve_to_interp[node.curve] + end line.keys = {} for j, key_node in ipairs(node) do local keyframe = { frame = tonumber(key_node.frame), value = tonumber(key_node.value), } - if line.interp == INT_BEZIER then + if line.interp == animation.INT_BEZIER then keyframe.lx = tonumber(key_node.lx) keyframe.ly = tonumber(key_node.ly) keyframe.rx = tonumber(key_node.rx) @@ -51,6 +55,7 @@ local function parse_track(root) duration = math.huge, fps = DEFAULT_FPS, linesets = {}, + curves = {}, } local linesets = raw_track.linesets for i, node in ipairs(root) do @@ -61,6 +66,9 @@ local function parse_track(root) if tag == "configure" then parse_configure(raw_track, node) goto continue + elseif tag == "curve" then + raw_track.curves[node.name] = node + goto continue end local target_type = nil @@ -98,7 +106,7 @@ end local function load_vca(source, filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) - return animation.compile_track(raw_track, filepath) + return internals.compile_animation_track(raw_track, filepath) end function internals.load_vca_animation(filepath, source, identifier) diff --git a/res/modules/internal/stdcomp.lua b/res/modules/internal/stdcomp.lua index 4a0ace852..dfea2e7c9 100644 --- a/res/modules/internal/stdcomp.lua +++ b/res/modules/internal/stdcomp.lua @@ -58,8 +58,8 @@ local Skeleton = {__index={ index=function(self, s) return __skeleton.index(self.eid, s) end, is_visible=function(self, i) return __skeleton.is_visible(self.eid, i) end, set_visible=function(self, i, b) return __skeleton.set_visible(self.eid, i, b) end, - get_color=function(self) return __skeleton.get_color(self.eid) end, - set_color=function(self, color) return __skeleton.set_color(self.eid, color) end, + get_color=function(self, ...) return __skeleton.get_color(self.eid, ...) end, + set_color=function(self, color, ...) return __skeleton.set_color(self.eid, color, ...) end, set_interpolated=function(self, b) return __skeleton.set_interpolated(self.eid, b) end, }} diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 280c5ddeb..4a8c8915b 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -98,40 +98,80 @@ local _udp_server_callbacks = {} local _udp_client_datagram_callbacks = {} local _udp_client_open_callbacks = {} local _http_response_callbacks = {} -local _http_error_callbacks = {} + +local http_request = network.__request +local open_tcp = network.__open_tcp +local open_udp = network.__open_udp +local connect_tcp = network.__connect_tcp +local connect_udp = network.__connect_udp +network.__request = nil +network.__open_tcp = nil +network.__open_udp = nil +network.__connect_tcp = nil +network.__connect_udp = nil + +local function request(url, params) + local id = http_request(url, params) + if params.on_response then + _http_response_callbacks[id] = params.on_response + end +end + +network.request = request network.get = function(url, callback, errorCallback, headers) - local id = network.__get(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + method = "GET", + headers = headers, + on_response = function(response) + if response.status / 100 == 2 then + return callback(response.body) + else + return errorCallback(response.status, response.body) + end + end, + on_error = function(response) + errorCallback(response.status, response.body) + end, + follow_location = true, + }) end network.get_binary = function(url, callback, errorCallback, headers) - local id = network.__get_binary(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + method = "GET", + headers = headers, + on_response = callback and (function (response) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end + end), + follow_location = true, + }) end -network.post = function(url, data, callback, errorCallback, headers) - local id = network.__post(url, data, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end +network.post = function(url, body, callback, errorCallback, headers) + return request(url, { + method = "POST", + headers = table.extend({ + "Content-Type: application/json" + }, headers), + body = body, + on_response = function(response) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end + end, + follow_location = true, + }) end network.tcp_open = function (port, handler) - local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket) + local socket = setmetatable({id=open_tcp(port)}, ServerSocket) _tcp_server_callbacks[socket.id] = function(id) handler(setmetatable({id=id}, Socket)) @@ -141,7 +181,7 @@ end network.tcp_connect = function(address, port, callback, errorCallback) local socket = setmetatable({id=0}, Socket) - socket.id = network.__connect_tcp(address, port) + socket.id = connect_tcp(address, port) _tcp_client_callbacks[socket.id] = function() callback(socket) end if errorCallback then _tcp_client_error_callbacks[socket.id] = function(message) errorCallback(socket, message) end @@ -154,7 +194,7 @@ network.udp_open = function (port, datagramHandler) error "udp server cannot be opened without datagram handler" end - local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket) + local socket = setmetatable({id=open_udp(port)}, DatagramServerSocket) _udp_server_callbacks[socket.id] = function(address, port, data) datagramHandler(address, port, data, socket) @@ -169,7 +209,7 @@ network.udp_connect = function (address, port, datagramHandler, openCallback) end local socket = setmetatable({id=0}, WriteableSocket) - socket.id = network.__connect_udp(address, port) + socket.id = connect_udp(address, port) _udp_client_datagram_callbacks[socket.id] = datagramHandler if openCallback then @@ -239,20 +279,10 @@ network.__process_events = function() end end elseif etype == RESPONSE then - if event[2] / 100 == 2 then - local callback = _http_response_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[4]) - end - else - local callback = _http_error_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[2], event[4]) - end + local callback = _http_response_callbacks[event[3]] + _http_response_callbacks[event[3]] = nil + if callback then + callback(event[4]) end end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index dee3bf579..3573bda07 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,7 @@ world.raycast = entities.__world_raycast entities.__world_raycast = nil animation = require "core:animation" +require "core:internal/animation_codegen" require "core:internal/formats/vca" __vc_scripts_registry = require "core:internal/scripts_registry" diff --git a/res/shaders/entity.glslv b/res/shaders/entity.glslv index 8f534a6dc..579a15879 100644 --- a/res/shaders/entity.glslv +++ b/res/shaders/entity.glslv @@ -2,7 +2,7 @@ layout (location = 0) in vec3 v_position; layout (location = 1) in vec2 v_texCoord; -layout (location = 2) in vec3 v_color; +layout (location = 2) in vec4 v_color; layout (location = 3) in vec4 v_light; layout (location = 4) in vec4 v_normal; @@ -29,8 +29,8 @@ void main() { a_dir = a_modelpos.xyz - u_cameraPos; vec3 skyLightColor = pick_sky_color(u_skybox, u_dayTime, u_minSkyLight); - a_color.rgb = max(a_color.rgb, skyLightColor.rgb * v_light.a) * v_color; - a_color.a = u_opacity; + a_color.rgb = max(a_color.rgb, skyLightColor.rgb * v_light.a) * v_color.rgb; + a_color.a = u_opacity * v_color.a; mat4 viewmodel = u_view * u_model; a_distance = length(viewmodel * vec4(pos3d, 0.0)); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f4969a29..623580241 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,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) @@ -86,12 +87,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/animation/rigging.cpp b/src/animation/rigging.cpp index d5e36a82c..7a4061f8a 100644 --- a/src/animation/rigging.cpp +++ b/src/animation/rigging.cpp @@ -46,7 +46,8 @@ Skeleton::Skeleton(std::shared_ptr config) flags(config ? config->getBones().size() : 0), textures(), modelOverrides(config ? config->getBones().size() : 0), - visible(true) { + visible(true), + boneTints(config ? config->getBones().size() : 0, glm::vec4(1.0f)) { if (config == nullptr) { return; } @@ -99,6 +100,7 @@ void Skeleton::setConfig(std::shared_ptr rigConfig) { modelOverrides.resize(bonesCount); flags.resize(bonesCount); + boneTints.resize(bonesCount, glm::vec4(1.0f)); for (size_t i = 0; i < bonesCount; i++) { flags[i].visible = true; @@ -175,6 +177,7 @@ void SkeletonConfig::render( const Assets& assets, ModelBatch& batch, Skeleton& skeleton, + ModelLightingMode lightingMode, const glm::mat3& rotation, const glm::vec3& position, const glm::vec3& scale @@ -201,16 +204,24 @@ void SkeletonConfig::render( if (auto foundOverride = modelOverride.model.lock()) { model = foundOverride.get(); } - if (model) { - batch.draw( - skeleton.calculated.matrices[i], - skeleton.tint, - model, - &skeleton.textures - ); - } else if (!node->model.name.empty()) { - node->model.updateFlag = true; - } + if (model == nullptr) { + if (!node->model.name.empty()) { + node->model.updateFlag = true; + } + continue; + } + batch.draw( + skeleton.calculated.matrices[i], + skeleton.tint * skeleton.boneTints[i], + lightingMode == ModelLightingMode::SOLID + ? position - glm::vec3( + skeleton.calculated.matrices[i] * + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) + ) + : glm::vec3(), + model, + &skeleton.textures + ); } } diff --git a/src/animation/rigging.hpp b/src/animation/rigging.hpp index 82066e132..35d7fd01b 100644 --- a/src/animation/rigging.hpp +++ b/src/animation/rigging.hpp @@ -11,6 +11,7 @@ #include "typedefs.hpp" #include "data/dv_fwd.hpp" #include "util/Interpolation.hpp" +#include "util/EnumMetadata.hpp" class Assets; class ModelBatch; @@ -19,6 +20,16 @@ namespace model { struct Model; } +enum class ModelLightingMode { + BONE_BASED, + SOLID, +}; + +VC_ENUM_METADATA(ModelLightingMode) + {"bone-based", ModelLightingMode::BONE_BASED}, + {"solid", ModelLightingMode::SOLID}, +VC_ENUM_END + namespace rigging { struct Skeleton; class SkeletonConfig; @@ -85,7 +96,8 @@ namespace rigging { std::unordered_map textures; std::vector modelOverrides; bool visible; - glm::vec3 tint {1.0f, 1.0f, 1.0f}; + glm::vec4 tint {1.0f, 1.0f, 1.0f, 1.0f}; + std::vector boneTints; util::VecInterpolation<3, float> interpolation {false}; @@ -131,6 +143,7 @@ namespace rigging { const Assets& assets, ModelBatch& batch, Skeleton& skeleton, + ModelLightingMode lightingMode, const glm::mat3& rotation, const glm::vec3& position, const glm::vec3& scale diff --git a/src/content/loading/EntityLoader.cpp b/src/content/loading/EntityLoader.cpp index e785fb0fe..625b1927a 100644 --- a/src/content/loading/EntityLoader.cpp +++ b/src/content/loading/EntityLoader.cpp @@ -94,4 +94,8 @@ template<> void ContentUnitLoader::loadUnit( root.at("mass").get(def.mass); root.at("elasticity").get(def.elasticity); root.at("step-height").get(def.stepHeight); + + std::string lightingModeName; + root.at("lighting-mode").get(lightingModeName); + ModelLightingModeMeta.getItem(lightingModeName, def.lightingMode); } diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp new file mode 100644 index 000000000..72f92366a --- /dev/null +++ b/src/crypto/Crypto.cpp @@ -0,0 +1,1247 @@ +#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"); + } + result.publicKey = ecPrivateKey( + curve, + std::string_view( + reinterpret_cast(result.privateKey.data()), + result.privateKey.size() + ) + ).publicKey; + 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/graphics/core/Framebuffer.cpp b/src/graphics/core/Framebuffer.cpp index 1dc677096..a10da01ad 100644 --- a/src/graphics/core/Framebuffer.cpp +++ b/src/graphics/core/Framebuffer.cpp @@ -117,6 +117,13 @@ std::shared_ptr Framebuffer::getSharedTexture() const { return texture; } +std::unique_ptr Framebuffer::readData() const { + if (!texture) { + return nullptr; + } + return texture->readData(); +} + uint Framebuffer::getWidth() const { return width; } diff --git a/src/graphics/core/Framebuffer.hpp b/src/graphics/core/Framebuffer.hpp index 5f9dc3491..00c1422a7 100644 --- a/src/graphics/core/Framebuffer.hpp +++ b/src/graphics/core/Framebuffer.hpp @@ -6,6 +6,7 @@ #include class Texture; +class ImageData; class Framebuffer : public Bindable { uint fbo; @@ -36,6 +37,8 @@ public: std::shared_ptr getSharedTexture() const; + std::unique_ptr readData() const; + /// @brief Get framebuffer width uint getWidth() const; /// @brief Get framebuffer height diff --git a/src/graphics/render/BlockWrapsRenderer.cpp b/src/graphics/render/BlockWrapsRenderer.cpp index 168ad14a2..7548afa34 100644 --- a/src/graphics/render/BlockWrapsRenderer.cpp +++ b/src/graphics/render/BlockWrapsRenderer.cpp @@ -161,7 +161,7 @@ void BlockWrapsRenderer::draw(const DrawContext& pctx) { u64id_t BlockWrapsRenderer::add( const glm::ivec3& position, const std::string& texture, - const glm::vec3& tint, + const glm::vec4& tint, float emission ) { u64id_t id = nextWrapper++; diff --git a/src/graphics/render/BlockWrapsRenderer.hpp b/src/graphics/render/BlockWrapsRenderer.hpp index 0f2ed8f7e..fb4893877 100644 --- a/src/graphics/render/BlockWrapsRenderer.hpp +++ b/src/graphics/render/BlockWrapsRenderer.hpp @@ -20,7 +20,7 @@ class DrawContext; struct BlockWrapper { glm::ivec3 position; std::array textureFaces {}; - std::array tints {}; + std::array tints {}; float emission = 0.0f; // --- render cache --- @@ -56,7 +56,7 @@ public: u64id_t add( const glm::ivec3& position, const std::string& texture, - const glm::vec3& tint, + const glm::vec4& tint, float emission ); diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index f75bd675c..8b8a667eb 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -357,20 +357,30 @@ void BlocksRenderer::blockCustomModel( const auto& vcoord = vertex.coord - 0.5f; glm::vec4 aoColor {1.0f, 1.0f, 1.0f, 1.0f}; - if (shading && ao) { + if (shading) { const float eps = 0.05f; auto p = coord + vcoord.x * X + vcoord.y * Y + vcoord.z * Z + - r * 0.5f + t * 0.5f + n * eps; - auto p1 = p + n * eps; - auto p2 = p + n * 0.5f; - aoColor = pickSoftLight(p1.x, p1.y, p1.z, glm::ivec3(r), glm::ivec3(t)); - if (!block.lightPassing) { - aoColor = glm::max( - aoColor, - pickSoftLight( - p2.x, p2.y, p2.z, glm::ivec3(r), glm::ivec3(t) - ) - ); + r * 0.5f + t * 0.5f + n * eps; + if (ao) { + auto p1 = p + n * eps; + auto p2 = p + n * 0.5f; + aoColor = pickSoftLight(p1.x, p1.y, p1.z, glm::ivec3(r), glm::ivec3(t)); + if (!block.lightPassing) { + aoColor = glm::max( + aoColor, + pickSoftLight( + p2.x, p2.y, p2.z, glm::ivec3(r), glm::ivec3(t) + ) + ); + } + } else { + aoColor = pickLight(glm::floor(p + 0.5f) + 0.5f); + if (!block.lightPassing) { + aoColor = glm::max( + aoColor, + pickLight(glm::floor(p + n + 0.5f) + 0.5f) + ); + } } } this->vertex( diff --git a/src/graphics/render/HandsRenderer.cpp b/src/graphics/render/HandsRenderer.cpp index 3fc21342a..27e43a078 100644 --- a/src/graphics/render/HandsRenderer.cpp +++ b/src/graphics/render/HandsRenderer.cpp @@ -30,6 +30,7 @@ void HandsRenderer::render(const Camera& camera) { assets, modelBatch, skeleton, + ModelLightingMode::SOLID, glm::mat3(1.0f), glm::vec3(), glm::vec3(1.0f) diff --git a/src/graphics/render/MainBatch.cpp b/src/graphics/render/MainBatch.cpp index 91364e510..70d4af6f7 100644 --- a/src/graphics/render/MainBatch.cpp +++ b/src/graphics/render/MainBatch.cpp @@ -82,12 +82,75 @@ inline glm::vec4 do_tint(float value) { return {value, value, value, 1.0f}; } +void MainBatch::quad( + const glm::vec3& pos, + const glm::vec3& right, + const glm::vec3& up, + const glm::vec3& normal, + const glm::vec2& size, + const glm::vec4& light, + const glm::vec4& tint, + const UVRegion& subregion, + float emission +) { + prepare(6); + vertex( + pos - right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u1, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u2, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u2, subregion.v2}, + light, + tint, + normal, + emission + ); + + vertex( + pos - right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u1, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u2, subregion.v2}, + light, + tint, + normal, + emission + ); + vertex( + pos - right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u1, subregion.v2}, + light, + tint, + normal, + emission + ); +} + void MainBatch::cube( const glm::vec3& coord, const glm::vec3& size, const UVRegion(&texfaces)[6], const glm::vec4& lights, - const glm::vec3 tints[], + const glm::vec4 tints[], float emission, uint8_t cullingBits ) { diff --git a/src/graphics/render/MainBatch.hpp b/src/graphics/render/MainBatch.hpp index 8ad119199..d51b2d05b 100644 --- a/src/graphics/render/MainBatch.hpp +++ b/src/graphics/render/MainBatch.hpp @@ -17,30 +17,20 @@ class Chunks; struct MainBatchVertex { glm::vec3 position; glm::vec2 uv; - glm::vec3 tint; + glm::vec4 tint; std::array color; std::array normal; static constexpr VertexAttribute ATTRIBUTES[] = { {VertexAttribute::Type::FLOAT, false, 3}, {VertexAttribute::Type::FLOAT, false, 2}, - {VertexAttribute::Type::FLOAT, false, 3}, + {VertexAttribute::Type::FLOAT, false, 4}, {VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {{}, 0}}; }; class MainBatch { - std::unique_ptr const buffer; - size_t const capacity; - size_t index; - - UVRegion region {0.0f, 0.0f, 1.0f, 1.0f}; - - std::unique_ptr> mesh; - std::unique_ptr blank; - - const Texture* texture = nullptr; public: /// xyz, uv, color, compressed lights @@ -63,7 +53,7 @@ public: const glm::vec3& pos, const glm::vec2& uv, const glm::vec4& light, - const glm::vec3& tint, + const glm::vec4& tint, const glm::vec3& normal, float emission ) { @@ -84,76 +74,36 @@ public: index++; } - inline void quad( + void quad( const glm::vec3& pos, const glm::vec3& right, const glm::vec3& up, const glm::vec3& normal, const glm::vec2& size, const glm::vec4& light, - const glm::vec3& tint, + const glm::vec4& tint, const UVRegion& subregion, float emission = 0.0f - ) { - prepare(6); - vertex( - pos - right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u1, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u2, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u2, subregion.v2}, - light, - tint, - normal, - emission - ); - - vertex( - pos - right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u1, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u2, subregion.v2}, - light, - tint, - normal, - emission - ); - vertex( - pos - right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u1, subregion.v2}, - light, - tint, - normal, - emission - ); - } + ); void cube( const glm::vec3& coord, const glm::vec3& size, const UVRegion(&texfaces)[6], const glm::vec4& lights, - const glm::vec3 tints[], + const glm::vec4 tints[], float emission, uint8_t cullingBits = 0xFF ); +private: + std::unique_ptr const buffer; + size_t const capacity; + size_t index; + + UVRegion region {0.0f, 0.0f, 1.0f, 1.0f}; + + std::unique_ptr> mesh; + std::unique_ptr blank; + + const Texture* texture = nullptr; }; diff --git a/src/graphics/render/ModelBatch.cpp b/src/graphics/render/ModelBatch.cpp index 35e891aff..35b5e6274 100644 --- a/src/graphics/render/ModelBatch.cpp +++ b/src/graphics/render/ModelBatch.cpp @@ -61,8 +61,8 @@ void ModelBatch::draw( const model::Mesh& mesh, const glm::mat4& matrix, const glm::mat3& rotation, - glm::vec3 tint, - const texture_names_map* varTextures, + const glm::vec4& tint, + const TextureNamesMap* varTextures, bool backlight ) { setTexture(mesh.texture, varTextures); @@ -72,7 +72,7 @@ void ModelBatch::draw( glm::vec4 lights(1, 1, 1, 0); if (mesh.shading) { glm::vec3 gpos = matrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - gpos += lightsOffset; + gpos += lightsOffset + localLightsOffset; lights = MainBatch::sampleLight(gpos, chunks, backlight); } for (size_t i = 0; i < vcount / 3; i++) { @@ -97,14 +97,21 @@ void ModelBatch::draw( } } -void ModelBatch::draw(glm::mat4 matrix, - glm::vec3 tint, - const model::Model* model, - const texture_names_map* varTextures) { +void ModelBatch::draw( + const glm::mat4& matrix, + const glm::vec4& tint, + const glm::vec3& lightSampleOffset, + const model::Model* model, + const TextureNamesMap* varTextures +) { for (const auto& mesh : model->meshes) { - entries.push_back({ - matrix, extract_rotation(matrix), tint, &mesh, varTextures - }); + entries.push_back(DrawEntry { + matrix, + extract_rotation(matrix), + tint, + lightSampleOffset, + &mesh, + varTextures}); } } @@ -116,6 +123,7 @@ void ModelBatch::render() { ); bool backlight = settings.graphics.backlight.get(); for (auto& entry : entries) { + localLightsOffset = entry.lightSampleOffset; draw( *entry.mesh, entry.matrix, @@ -124,6 +132,7 @@ void ModelBatch::render() { entry.varTextures, backlight ); + localLightsOffset = {}; } batch->flush(); entries.clear(); @@ -134,7 +143,7 @@ void ModelBatch::setLightsOffset(const glm::vec3& offset) { } void ModelBatch::setTexture(const std::string& name, - const texture_names_map* varTextures) { + const TextureNamesMap* varTextures) { if (varTextures && !name.empty() && name.at(0) == '$') { const auto& found = varTextures->find(name); if (found == varTextures->end()) { diff --git a/src/graphics/render/ModelBatch.hpp b/src/graphics/render/ModelBatch.hpp index e93237865..5d43024d5 100644 --- a/src/graphics/render/ModelBatch.hpp +++ b/src/graphics/render/ModelBatch.hpp @@ -18,37 +18,9 @@ namespace model { struct Model; } -using texture_names_map = std::unordered_map; +using TextureNamesMap = std::unordered_map; class ModelBatch { - const Assets& assets; - const Chunks& chunks; - - const EngineSettings& settings; - glm::vec3 lightsOffset {}; - - static inline glm::vec3 SUN_VECTOR {0.411934f, 0.863868f, -0.279161f}; - - std::unique_ptr batch; - - void draw(const model::Mesh& mesh, - const glm::mat4& matrix, - const glm::mat3& rotation, - glm::vec3 tint, - const texture_names_map* varTextures, - bool backlight); - - void setTexture(const std::string& name, - const texture_names_map* varTextures); - - struct DrawEntry { - glm::mat4 matrix; - glm::mat3 rotation; - glm::vec3 tint; - const model::Mesh* mesh; - const texture_names_map* varTextures; - }; - std::vector entries; public: ModelBatch( size_t capacity, @@ -58,11 +30,47 @@ public: ); ~ModelBatch(); - void draw(glm::mat4 matrix, - glm::vec3 tint, - const model::Model* model, - const texture_names_map* varTextures); + void draw( + const glm::mat4& matrix, + const glm::vec4& tint, + const glm::vec3& lightSampleOffset, + const model::Model* model, + const TextureNamesMap* varTextures + ); void render(); void setLightsOffset(const glm::vec3& offset); +private: + const Assets& assets; + const Chunks& chunks; + + const EngineSettings& settings; + glm::vec3 lightsOffset {}; + glm::vec3 localLightsOffset {}; + + static inline glm::vec3 SUN_VECTOR {0.411934f, 0.863868f, -0.279161f}; + + std::unique_ptr batch; + + void draw( + const model::Mesh& mesh, + const glm::mat4& matrix, + const glm::mat3& rotation, + const glm::vec4& tint, + const TextureNamesMap* varTextures, + bool backlight + ); + + void setTexture(const std::string& name, + const TextureNamesMap* varTextures); + + struct DrawEntry { + glm::mat4 matrix; + glm::mat3 rotation; + glm::vec4 tint; + glm::vec3 lightSampleOffset; + const model::Mesh* mesh; + const TextureNamesMap* varTextures; + }; + std::vector entries; }; diff --git a/src/graphics/render/ParticlesRenderer.cpp b/src/graphics/render/ParticlesRenderer.cpp index d588cd63a..ea449479c 100644 --- a/src/graphics/render/ParticlesRenderer.cpp +++ b/src/graphics/render/ParticlesRenderer.cpp @@ -160,7 +160,7 @@ void ParticlesRenderer::renderParticle( -camera.front, preset.size * scale, light, - glm::vec3(1.0f), + glm::vec4(1.0f), particle.region, preset.lighting ? 0.0f : 1.0f ); diff --git a/src/graphics/render/PrecipitationRenderer.cpp b/src/graphics/render/PrecipitationRenderer.cpp index c566a06d2..6b2735d54 100644 --- a/src/graphics/render/PrecipitationRenderer.cpp +++ b/src/graphics/render/PrecipitationRenderer.cpp @@ -151,7 +151,7 @@ void PrecipitationRenderer::render( glm::cross(glm::vec3(0, 1, 0), face.right), FACE_SIZE, light_at(chunks, pos.x, y, pos.z), - glm::vec3(1.0f), + glm::vec4(1.0f), calc_uv(pos, face.right, timer, weather) ); } diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 5a0260f62..8070d58b0 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -374,6 +374,14 @@ std::shared_ptr GUI::getActiveFrame() const { return activeFrame; } +std::shared_ptr GUI::getFrame(const std::string& id) { + const auto& found = frames.find(id); + if (found == frames.end()) { + return nullptr; + } + return found->second; +} + void GUI::remove(UINode* node) noexcept { container->remove(node); } diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index 97fc0ed37..997b67bea 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -137,6 +137,8 @@ namespace gui { std::shared_ptr getActiveFrame() const; + std::shared_ptr getFrame(const std::string& id); + /// @brief Remove node from the main container void remove(UINode* node) noexcept; diff --git a/src/graphics/ui/elements/Frame.cpp b/src/graphics/ui/elements/Frame.cpp index 0387f5c1e..fe3562b4f 100644 --- a/src/graphics/ui/elements/Frame.cpp +++ b/src/graphics/ui/elements/Frame.cpp @@ -66,3 +66,10 @@ const std::string& gui::Frame::getOutputTexture() const { const std::string& gui::Frame::getFrameId() const { return frameId; } + +std::unique_ptr gui::Frame::takeScreenshot() const { + if (fbo == nullptr) { + return nullptr; + } + return fbo->readData(); +} diff --git a/src/graphics/ui/elements/Frame.hpp b/src/graphics/ui/elements/Frame.hpp index 1baa7bc91..f7513324b 100644 --- a/src/graphics/ui/elements/Frame.hpp +++ b/src/graphics/ui/elements/Frame.hpp @@ -4,6 +4,7 @@ class Framebuffer; class UiDocument; +class ImageData; namespace gui { class Frame final : public Container { @@ -18,6 +19,8 @@ namespace gui { const std::string& getOutputTexture() const; const std::string& getFrameId() const; + + std::unique_ptr takeScreenshot() const; private: std::string frameId; std::unique_ptr fbo; 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/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index a310abe87..c1004434f 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -147,8 +147,19 @@ static int l_set_visible(lua::State* L) { return 0; } +static glm::vec4 to_color(lua::State* L, int idx) { + if (lua::objlen(L, idx) > 3) { + return lua::tovec4(L, idx); + } + return glm::vec4(lua::tovec3(L, idx), 1.0f); +} + static int l_get_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { + if (lua::isnumber(L, 2)) { + auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); + return lua::pushvec(L, skeleton->boneTints[index]); + } return lua::pushvec(L, skeleton->tint); } return 0; @@ -156,7 +167,13 @@ static int l_get_color(lua::State* L) { static int l_set_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { - skeleton->tint = lua::tovec3(L, 2); + auto color = to_color(L, 2); + if (lua::isnumber(L, 3)) { + auto index = index_range_check(*skeleton, lua::tointeger(L, 3)); + skeleton->boneTints[index] = color; + } else { + skeleton->tint = color; + } } return 0; } diff --git a/src/logic/scripting/lua/libs/libblockwraps.cpp b/src/logic/scripting/lua/libs/libblockwraps.cpp index 084dd6b8b..a6eba3e8f 100644 --- a/src/logic/scripting/lua/libs/libblockwraps.cpp +++ b/src/logic/scripting/lua/libs/libblockwraps.cpp @@ -10,7 +10,11 @@ static int l_wrap(lua::State* L) { auto position = lua::tovec3(L, 1); std::string texture = lua::require_string(L, 2); float emission = lua::isnumber(L, 3) ? lua::tonumber(L, 3) : 1.0f; - glm::vec3 tint = lua::istable(L, 4) ? lua::tovec3(L, 4) : glm::vec3(1.0f); + glm::vec4 tint = + lua::istable(L, 4) + ? (lua::objlen(L, 4) > 3 ? lua::tovec4(L, 4) + : glm::vec4(lua::tovec3(L, 4), 1.0f)) + : glm::vec4(1.0f); return lua::pushinteger( L, @@ -72,9 +76,12 @@ static int l_set_tints(lua::State* L) { if (auto wrapper = renderer->blockWraps->get(lua::tointeger(L, 1))) { for (int i = 0; i < wrapper->textureFaces.size(); i++) { if (lua::isnil(L, 2 + i)) { - wrapper->tints[i] = glm::vec3(1.0f); + wrapper->tints[i] = glm::vec4(1.0f); } else { - wrapper->tints[i] = lua::tovec3(L, 2 + i); + wrapper->tints[i] = + lua::objlen(L, 2 + i) > 3 + ? lua::tovec4(L, 2 + i) + : glm::vec4(lua::tovec3(L, 2 + i), 1.0f); } } } 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/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 81e5fc4f7..389bedc9a 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -25,6 +25,7 @@ #include "items/Inventories.hpp" #include "util/stringutil.hpp" #include "world/Level.hpp" +#include "window/Window.hpp" #include "../usertypes/lua_type_canvas.hpp" using namespace gui; @@ -1218,7 +1219,26 @@ static int l_get_active_frame(lua::State* L) { return lua::pushstring(L, frame->getId()); } +static int l_screenshot(lua::State* L) { + if (engine->isHeadless()) { + return 0; + } + std::unique_ptr image; + if (lua::isstring(L, 1)) { + auto& gui = engine->getGUI(); + auto frame = gui.getFrame(lua::require_string(L, 1)); + if (frame == nullptr) { + return 0; + } + image = frame->takeScreenshot(); + } else { + image = engine->getWindow().takeScreenshot(); + } + return lua::newuserdata(L, nullptr, std::move(image)); +} + const luaL_Reg guilib[] = { + {"screenshot", lua::wrap}, {"get_viewport", lua::wrap}, {"getattr", lua::wrap}, {"setattr", lua::wrap}, diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 55db6e550..59d50f075 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -3,6 +3,7 @@ #include "engine/Engine.hpp" #include "network/Network.hpp" #include "devtools/Project.hpp" +#include "util/stringutil.hpp" #include #include @@ -27,7 +28,7 @@ struct ResponseEventDto { int status; bool binary; int requestId; - std::vector bytes; + network::HttpResponse response; }; enum NetworkDatagramSide { @@ -85,74 +86,55 @@ static std::vector read_headers(lua::State* L, int index) { static int request_id = 1; -static int perform_get(lua::State* L, network::Network& network, bool binary) { - std::string url(lua::require_lstring(L, 1)); - auto headers = read_headers(L, 2); +static int l_request(lua::State* L, network::Network& network) { + network::HttpRequest request {}; + request.url = lua::require_lstring(L, 1); - int currentRequestId = request_id++; - - network.get( - url, - [currentRequestId, binary](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, binary, currentRequestId, std::move(bytes)} - )); - }, - [currentRequestId, binary](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, binary, currentRequestId, std::move(bytes)} - )); - }, - std::move(headers) - ); - return lua::pushinteger(L, currentRequestId); -} - -static int l_get(lua::State* L, network::Network& network) { - return perform_get(L, network, false); -} - -static int l_get_binary(lua::State* L, network::Network& network) { - return perform_get(L, network, true); -} - -static int l_post(lua::State* L, network::Network& network) { - std::string url(lua::require_lstring(L, 1)); - auto data = lua::tovalue(L, 2); - - std::string string; - if (data.isString()) { - string = data.asString(); - } else { - string = json::stringify(data, false); + if (!lua::istable(L, 2)) { + throw std::runtime_error("table expected as argument #2"); + } + if (lua::getfield(L, "method", 2)) { + request.method = lua::require_string(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "headers", 2)) { + request.headers = read_headers(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "body", 2)) { + if (lua::type(L, -1) == LUA_TCDATA) { + request.body = lua::bytearray_as_string(L, -1); + } else { + request.body = lua::require_lstring(L, -1); + } + lua::pop(L); + } + if (lua::getfield(L, "follow_location", 2)) { + request.followLocation = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "timeout_ms", 2)) { + request.timeoutMs = lua::tointeger(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "verify_ssl", 2)) { + request.verifySSL = lua::toboolean(L, -1); + lua::pop(L); } - auto headers = read_headers(L, 3); int currentRequestId = request_id++; + request.onResponse = [currentRequestId](network::HttpResponse response) { + push_event(NetworkEvent( + RESPONSE, + ResponseEventDto { + response.status, + false, + currentRequestId, + std::move(response)} + )); + }; - network.post( - url, - string, - [currentRequestId](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, false, currentRequestId, std::move(bytes)} - )); - }, - [currentRequestId](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, false, currentRequestId, std::move(bytes)} - )); - }, - std::move(headers) - ); + network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); } @@ -519,11 +501,31 @@ static int l_pull_events(lua::State* L) { lua::pushinteger(L, dto.requestId); lua::rawseti(L, 3); + lua::createtable(L, 0, 2); + lua::pushinteger(L, dto.response.status); + lua::setfield(L, "status"); + if (dto.binary) { - lua::create_bytearray(L, dto.bytes.data(), dto.bytes.size()); + lua::create_bytearray( + L, dto.response.body.data(), dto.response.body.size() + ); } else { - lua::pushlstring(L, std::string_view(dto.bytes.data(), dto.bytes.size())); + lua::pushlstring( + L, + std::string_view( + dto.response.body.data(), dto.response.body.size() + ) + ); } + lua::setfield(L, "body"); + + lua::createtable(L, dto.response.headers.size(), 0); + for (int i = 0; i < dto.response.headers.size(); i++) { + lua::pushlstring(L, dto.response.headers[i]); + lua::rawseti(L, i + 1); + } + lua::setfield(L, "headers"); + lua::rawseti(L, 4); break; } @@ -562,9 +564,7 @@ int wrap(lua_State* L) { } const luaL_Reg networklib[] = { - {"__get", wrap}, - {"__get_binary", wrap}, - {"__post", wrap}, + {"__request", wrap}, {"get_total_upload", wrap}, {"get_total_download", wrap}, {"find_free_port", wrap}, diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index b85608260..812f67665 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/src/network/Curl.cpp b/src/network/Curl.cpp index 1caafde73..0e08162fb 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -4,7 +4,7 @@ #define NOMINMAX #include -#include +#include using namespace network; @@ -23,97 +23,96 @@ static size_t write_callback( return size * nmemb; } -enum class RequestType { - GET, POST -}; +static size_t header_callback( + char* buffer, size_t size, size_t nitems, void* userdata +) { + auto* headers = static_cast*>(userdata); + size_t len = size * nitems; + std::string header(buffer, len); -struct Request { - RequestType type; - std::string url; - OnResponse onResponse; - OnReject onReject; - long maxSize; - bool followLocation = false; - std::string data; + while (!header.empty() && + (header.back() == '\r' || header.back() == '\n')) { + header.pop_back(); + } + + headers->push_back(std::move(header)); + return len; +} + +struct ProcessingRequest { + CURLM* multiHandle; + CURL* curl; + HttpRequest request; + std::vector buffer; std::vector headers; + + ProcessingRequest(CURLM* multiHandle) : multiHandle(multiHandle) { + curl = curl_easy_init(); + } + + ProcessingRequest(const ProcessingRequest&) = delete; + ProcessingRequest& operator=(const ProcessingRequest&) = delete; + + ProcessingRequest(ProcessingRequest&& other) noexcept + : multiHandle(other.multiHandle), + curl(other.curl), + request(std::move(other.request)), + buffer(std::move(other.buffer)), + headers(std::move(other.headers)) { + other.curl = nullptr; + other.multiHandle = nullptr; + } + + ProcessingRequest& operator=(ProcessingRequest&& other) noexcept { + if (this != &other) { + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } + multiHandle = other.multiHandle; + curl = other.curl; + request = std::move(other.request); + buffer = std::move(other.buffer); + headers = std::move(other.headers); + other.curl = nullptr; + other.multiHandle = nullptr; + } + return *this; + } + + ~ProcessingRequest() { + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } + } }; class CurlRequests : public Requests { CURLM* multiHandle; - CURL* curl; + std::vector> requests; size_t totalUpload = 0; size_t totalDownload = 0; - - OnResponse onResponse; - OnReject onReject; - std::vector buffer; - std::string url; - - std::queue requests; public: - CurlRequests(CURLM* multiHandle, CURL* curl) - : multiHandle(multiHandle), curl(curl) { + CurlRequests(CURLM* multiHandle) : multiHandle(multiHandle) { } virtual ~CurlRequests() { - curl_multi_remove_handle(multiHandle, curl); - curl_easy_cleanup(curl); + requests.clear(); curl_multi_cleanup(multiHandle); } - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize - ) override { - Request request { - RequestType::GET, - url, - onResponse, - onReject, - maxSize, - true, - "", - std::move(headers)}; + + void request(HttpRequest request) override { processRequest(std::move(request)); } - void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) override { - Request request { - RequestType::POST, - url, - onResponse, - onReject, - maxSize, - false, - "", - std::move(headers)}; - request.data = data; - processRequest(std::move(request)); - } + void processRequest(HttpRequest request) { + auto entry = std::make_unique(multiHandle); + auto curl = entry->curl; - void processRequest(Request request) { - if (!url.empty()) { - requests.push(request); - return; - } - onResponse = request.onResponse; - onReject = request.onReject; - url = request.url; - - buffer.clear(); - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POST, request.type == RequestType::POST); + curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str()); curl_slist* hs = nullptr; @@ -121,22 +120,25 @@ public: hs = curl_slist_append(hs, header.c_str()); } - switch (request.type) { - case RequestType::GET: - break; - case RequestType::POST: - hs = curl_slist_append(hs, "Content-Type: application/json"); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.data.length()); - curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.data.c_str()); - break; - default: - throw std::runtime_error("not implemented"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(request.body.length())); + if (!request.body.empty()) { + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data()); } + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, static_cast(request.verifySSL)); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, static_cast(request.verifySSL)); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, static_cast(request.followLocation)); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &entry->headers); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); + if (request.timeoutMs > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, request.timeoutMs); + } +#ifndef NDEBUG + // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); +#endif if (request.maxSize == 0) { curl_easy_setopt( curl, CURLOPT_MAXFILESIZE, std::numeric_limits::max() @@ -144,69 +146,105 @@ public: } else { curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, request.maxSize); } - curl_multi_add_handle(multiHandle, curl); - int running; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { - auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); + { + CURLMcode res = curl_multi_add_handle(multiHandle, curl); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; } - url = ""; } + { + int running; + CURLMcode res = curl_multi_perform(multiHandle, &running); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; + } + } + entry->request = std::move(request); + requests.push_back(std::move(entry)); } void update() override { - int messagesLeft; - int running; - CURLMsg* msg; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { + if (CURLMcode res = curl_multi_poll( + multiHandle, + nullptr, + 0, + 0, + nullptr + )) { auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); - } - curl_multi_remove_handle(multiHandle, curl); - url = ""; + logger.error() << message; return; } - if ((msg = curl_multi_info_read(multiHandle, &messagesLeft)) != nullptr) { - if(msg->msg == CURLMSG_DONE) { - curl_multi_remove_handle(multiHandle, curl); - } - int response; - curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); - if (response == HTTP_OK) { - long size; - if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { - totalUpload += size; - } - if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { - totalDownload += size; - } - totalDownload += buffer.size(); - if (onResponse) { - onResponse(std::move(buffer)); - } - } else { - logger.error() - << "response code " << response << " (" << url << ")" - << (buffer.empty() - ? "" - : std::to_string(buffer.size()) + " byte(s)"); - totalDownload += buffer.size(); - if (onReject) { - onReject(response, std::move(buffer)); - } - } - url = ""; + + int messagesLeft; + int running; + if (CURLMcode res = curl_multi_perform(multiHandle, &running)) { + auto message = curl_multi_strerror(res); + logger.error() << message; + return; } - if (url.empty() && !requests.empty()) { - auto request = std::move(requests.front()); - requests.pop(); - processRequest(std::move(request)); + CURLMsg* msg = curl_multi_info_read(multiHandle, &messagesLeft); + if (msg == nullptr || msg->msg != CURLMSG_DONE) { + return; + } + auto curl = msg->easy_handle; + auto found = std::find_if( + requests.begin(), + requests.end(), + [curl](const std::unique_ptr& entry) { + return entry && entry->curl == curl; + } + ); + if (requests.empty() || found == requests.end()) { + logger.error() << "could not find request for cURL handle"; + return; + } + auto entry = std::move(*found); + auto& req = entry->request; + requests.erase(found); + + long response = -1; + CURLcode result = msg->data.result; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); + auto headers = std::move(entry->headers); + if (response == 0) { + auto message = std::string(curl_easy_strerror(result)); + logger.error() << message << " (" << req.url << ")"; + if (req.onResponse) { + req.onResponse( + {static_cast(response), + std::move(headers), + std::vector( + message.data(), message.data() + message.size() + )} + ); + } + } else { + long size; + if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { + totalUpload += size; + } + if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { + totalDownload += size; + } + totalDownload += entry->buffer.size(); + if (req.onResponse) { + req.onResponse({ + static_cast(response), + std::move(headers), + std::move(entry->buffer), + }); + } } } @@ -219,16 +257,11 @@ public: } static std::unique_ptr create() { - auto curl = curl_easy_init(); - if (curl == nullptr) { - throw std::runtime_error("could not initialzie cURL"); - } auto multiHandle = curl_multi_init(); if (multiHandle == nullptr) { - curl_easy_cleanup(curl); - throw std::runtime_error("could not initialzie cURL-multi"); + throw std::runtime_error("could not initialize cURL-multi"); } - return std::make_unique(multiHandle, curl); + return std::make_unique(multiHandle); } }; diff --git a/src/network/Network.cpp b/src/network/Network.cpp index a0e1eb606..7b526a29a 100644 --- a/src/network/Network.cpp +++ b/src/network/Network.cpp @@ -51,27 +51,8 @@ Network::Network(std::unique_ptr requests) Network::~Network() = default; -void Network::get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->get(url, onResponse, onReject, std::move(headers), maxSize); -} - -void Network::post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->post( - url, fieldsData, onResponse, onReject, std::move(headers), maxSize - ); +void Network::request(HttpRequest request) { + requests->request(std::move(request)); } Connection* Network::getConnection(u64id_t id, bool includePrivate) { diff --git a/src/network/Network.hpp b/src/network/Network.hpp index 6d5cd6d54..49f44af6c 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -68,22 +68,7 @@ namespace network { Network(std::unique_ptr requests); ~Network(); - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); - - void post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); + void request(HttpRequest request); [[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate); [[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const; diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 8b834bb3e..cd98fbe00 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -10,33 +10,38 @@ #include namespace network { - using OnResponse = std::function)>; - using OnReject = std::function)>; + struct HttpResponse; + + using OnResponse = std::function; using ConnectCallback = std::function; using ConnectErrorCallback = std::function; using ServerDatagramCallback = std::function; using ClientDatagramCallback = std::function; + struct HttpRequest { + std::string method; + std::string url; + std::string body; + std::vector headers; + + OnResponse onResponse; + bool followLocation = false; + bool verifySSL = true; + long maxSize = -1; + long timeoutMs = 0; + }; + + struct HttpResponse { + int status; + std::vector headers; + std::vector body; + }; + class Requests { public: virtual ~Requests() {} - virtual void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; - - virtual void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; + virtual void request(HttpRequest request) = 0; [[nodiscard]] virtual size_t getTotalUpload() const = 0; [[nodiscard]] virtual size_t getTotalDownload() const = 0; diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 1d2ecc037..c2d476144 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -485,6 +485,7 @@ void Entities::render( if (eid.uid == fpsEntity) { continue; } + const auto& def = eid.def; const auto& pos = transform.pos; const auto& size = transform.size; if (frustum && !frustum->isBoxVisible(pos - size, pos + size)) { @@ -494,7 +495,13 @@ void Entities::render( const auto& rigConfig = skeleton.config; if (rigConfig) { rigConfig->render( - assets, batch, skeleton, transform.rot, pos, size + assets, + batch, + skeleton, + def.lightingMode, + transform.rot, + pos, + size ); } } diff --git a/src/objects/EntityDef.cpp b/src/objects/EntityDef.cpp index ae1f87c68..54e0aefef 100644 --- a/src/objects/EntityDef.cpp +++ b/src/objects/EntityDef.cpp @@ -14,4 +14,5 @@ void EntityDef::cloneTo(EntityDef& dst) { dst.elasticity = elasticity; dst.stepHeight = stepHeight; dst.save = save; + dst.lightingMode = lightingMode; } diff --git a/src/objects/EntityDef.hpp b/src/objects/EntityDef.hpp index 59aed2d7e..139bce48c 100644 --- a/src/objects/EntityDef.hpp +++ b/src/objects/EntityDef.hpp @@ -8,10 +8,7 @@ #include "data/dv.hpp" #include "maths/aabb.hpp" #include "physics/Hitbox.hpp" - -namespace rigging { - class SkeletonConfig; -} +#include "animation/rigging.hpp" struct ComponentInstance { std::string component; @@ -42,6 +39,9 @@ struct EntityDef { /// @brief Solid entity material material std::string material; + /// @brief Model lighting mode + ModelLightingMode lightingMode = ModelLightingMode::SOLID; + /// @brief Can the entity be interacted with cursor (on_attack, on_use) bool selectable = true; diff --git a/src/util/stringutil.cpp b/src/util/stringutil.cpp index 55529a398..8d80a82ab 100644 --- a/src/util/stringutil.cpp +++ b/src/util/stringutil.cpp @@ -543,6 +543,14 @@ static std::basic_string upper_case(const std::basic_string& str) return result; } +std::string util::lower_case(const std::string& str) { + return ::lower_case(str); +} + +std::string util::upper_case(const std::string& str) { + return ::upper_case(str); +} + std::wstring util::lower_case(const std::wstring& str) { return ::lower_case(str); } diff --git a/src/util/stringutil.hpp b/src/util/stringutil.hpp index c26f02741..3f2263efb 100644 --- a/src/util/stringutil.hpp +++ b/src/util/stringutil.hpp @@ -101,6 +101,8 @@ namespace util { std::string capitalized(const std::string& str); + std::string lower_case(const std::string& str); + std::string upper_case(const std::string& str); std::wstring lower_case(const std::wstring& str); std::wstring upper_case(const std::wstring& str); std::wstring capitalized(const std::wstring& str); diff --git a/test/crypto/Crypto.cpp b/test/crypto/Crypto.cpp new file mode 100644 index 000000000..b9153323b --- /dev/null +++ b/test/crypto/Crypto.cpp @@ -0,0 +1,479 @@ +#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; + std::size_t coordinateSize; + }; + const Case cases[] { + {"P-256", "prime256v1", EVP_sha256(), "SHA256", 32}, + {"P-384", "secp384r1", EVP_sha384(), "SHA384", 48}, + {"P-521", "secp521r1", EVP_sha512(), "SHA512", 66} + }; + 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); + BIGNUM* rawX = nullptr; + BIGNUM* rawY = nullptr; + ASSERT_EQ( + EVP_PKEY_get_bn_param(key.get(), OSSL_PKEY_PARAM_EC_PUB_X, &rawX), 1 + ); + ASSERT_EQ( + EVP_PKEY_get_bn_param(key.get(), OSSL_PKEY_PARAM_EC_PUB_Y, &rawY), 1 + ); + std::unique_ptr x(rawX, BN_free); + std::unique_ptr y(rawY, BN_free); + crypto::Bytes publicKey(item.coordinateSize * 2 + 1); + publicKey[0] = 0x04; + ASSERT_EQ( + BN_bn2binpad( + x.get(), publicKey.data() + 1, item.coordinateSize + ), + static_cast(item.coordinateSize) + ); + ASSERT_EQ( + BN_bn2binpad( + y.get(), + publicKey.data() + 1 + item.coordinateSize, + item.coordinateSize + ), + static_cast(item.coordinateSize) + ); + 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/network/curltest.cpp b/test/network/curltest.cpp index 42806c77c..e7fc4840d 100644 --- a/test/network/curltest.cpp +++ b/test/network/curltest.cpp @@ -6,18 +6,21 @@ TEST(curltest, curltest) { NetworkSettings settings {}; auto network = network::Network::create(settings); - network->get( - "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" - "heads/curl/res/content/base/blocks/lamp.json", - [](std::vector data) { - if (data.empty()) { - return; - } - auto view = std::string_view(data.data(), data.size()); - auto value = json::parse(view); - std::cout << value << std::endl; - }, [](auto, auto){} - ); + + network::HttpRequest request {}; + request.url = "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" + "heads/curl/res/content/base/blocks/lamp.json"; + request.onResponse = [](network::HttpResponse response) { + if (response.body.empty()) { + return; + } + auto view = + std::string_view(response.body.data(), response.body.size()); + auto value = json::parse(view); + std::cout << value << std::endl; + }; + network->request(std::move(request)); + std::cout << "upload: " << network->getTotalUpload() << " B" << std::endl; std::cout << "download: " << network->getTotalDownload() << " B" << std::endl; } 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" ] } diff --git a/vctest/main.cpp b/vctest/main.cpp index ee2f15e5b..99eebaacd 100644 --- a/vctest/main.cpp +++ b/vctest/main.cpp @@ -16,7 +16,8 @@ struct Config { fs::path directory; fs::path resDir {"res"}; fs::path workingDir {"."}; - std::string memchecker = "valgrind"; + std::string memchecker = ""; + std::string debugger = ""; bool outputAlways = false; }; @@ -30,6 +31,7 @@ static bool perform_keyword( std::cout << " --tests , -d = tests directory path\n"; std::cout << " --res , -r = 'res' directory path\n"; std::cout << " --user , -u = user directory path\n"; + std::cout << " --debugger = debugger with run command\n"; std::cout << " --memchecker = path to valgrind\n"; std::cout << " --output-always = always show tests output\n"; std::cout << std::endl; @@ -44,6 +46,8 @@ static bool perform_keyword( config.workingDir = fs::path(reader.next()); } else if (keyword == "--output-always") { config.outputAlways = true; + } else if (keyword == "--debugger") { + config.debugger = reader.next(); } else if (keyword == "--memchecker") { config.memchecker = reader.next(); } else { @@ -172,7 +176,7 @@ static std::string fix_path(std::string s) { return s; } -static bool run_test(const Config& config, const fs::path& path, bool memcheck = false) { +static bool run_test(const Config& config, const fs::path& path, bool debugRun = false) { using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::milliseconds; @@ -182,9 +186,16 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = auto name = path.stem(); std::stringstream ss; - if (memcheck) { - ss << config.memchecker << " --log-file=" - << fix_path(memcheckLogFile.string()) << " "; + if (debugRun) { + if (!config.memchecker.empty()) { + ss << config.memchecker; + ss << " --log-file=" << fix_path(memcheckLogFile.string()); + ss << " "; + } + if (!config.debugger.empty()) { + ss << config.debugger; + ss << " "; + } } ss << fs::canonical(config.executable) << " --headless"; ss << " --test " << fix_path(path.string()); @@ -203,7 +214,7 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = .count(); if (code) { - if (memcheck) { + if (debugRun) { // valgrind-specific output display_segfault_valgrind(memcheckLogFile, name, std::cerr); fs::remove(memcheckLogFile);