add crypto library

This commit is contained in:
dagger 2026-08-22 10:50:08 +07:00 • committed by dagger
parent 2c6cffcf19
commit a23c4d4159
17 changed files with 2839 additions and 7 deletions

View file

@ -31,7 +31,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev libglew2.2 \
libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev \
libcurl4-openssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind
libcurl4-openssl-dev libssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind
# fix luajit paths
sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a
sudo ln -s /usr/include/luajit-2.1 /usr/include/lua
@ -46,7 +46,7 @@ jobs:
- name: Build
run: cmake --build build -t install
- name: Run tests
run: ctest --test-dir build
run: ctest --output-on-failure --test-dir build
- name: Run engine tests
timeout-minutes: 1
run: |

View file

@ -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

View file

@ -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/*

View file

@ -15,6 +15,7 @@ Subsections:
- [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)

View file

@ -0,0 +1,140 @@
# *crypto* library
The library provides common cryptographic functions for mods
TLS and HTTP are not part of this library. They can use crypto for expensive
operations
Keys, signatures, hashes and encrypted data are passed as regular Lua strings.
Strings may contain zero bytes
The current API version is available in `crypto.API_VERSION`
## Hashing
```lua
crypto.sha256(data: str) -> str
crypto.sha384(data: str) -> str
crypto.sha512(data: str) -> str
crypto.md5(data: str) -> str
crypto.hash(hash: str, data: str) -> str
crypto.hmac(hash: str, key: str, data: str) -> str
```
Supported names are `SHA256`, `SHA384`, `SHA512` and `MD5`
MD5 is provided for old formats. SHA256 should be used for new data
Large data can be hashed in parts
```lua
local hash = crypto.hash_new("SHA256")
hash:update(part1)
hash:update(part2)
local result = hash:final()
```
Call `reset` before using the context again
## Signatures
```lua
crypto.ed25519_keypair() -> private_key, public_key
crypto.ed25519_public(private_key: str) -> str
crypto.ed25519_sign(private_key: str, message: str) -> str
crypto.ed25519_verify(public_key: str, message: str, signature: str)
-> true
-> false, error
crypto.ecdsa_keypair(curve: str) -> private_key, public_key
crypto.ecdsa_public(curve: str, private_key: str) -> str
crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str
crypto.ecdsa_verify(curve: str, public_key: str, message: str,
signature: str, hash: str)
-> true
-> false, error
crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str,
message: str, signature: str)
-> true
-> false, error
crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str,
message: str, signature: str, salt_length: int)
-> true
-> false, error
```
Ed25519 keys are 32 bytes and signatures are 64 bytes
ECDSA supports `P-256`, `P-384` and `P-521`. Public keys use the uncompressed
SEC1 format and signatures use DER
RSA modulus and exponent use big endian. A `salt_length` of `-1` uses the hash
size. RSA key generation is not provided, Ed25519 is easier for new keys
## Key exchange
```lua
crypto.x25519_keypair() -> private_key, public_key
crypto.x25519_public(private_key: str) -> str
crypto.x25519(private_key: str, peer_public_key: str) -> str
crypto.x25519_shared(private_key: str, peer_public_key: str) -> str
crypto.p256_keypair() -> private_key, public_key
crypto.p256_public(private_key: str) -> str
crypto.p256_shared(private_key: str, peer_public_key: str) -> str
```
`x25519` and `x25519_shared` do the same operation
Pass the shared secret through HKDF before using it as a key
## Encryption
```lua
crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str
crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str)
-> plaintext
-> nil, error
crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str,
plaintext: str) -> str
crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str,
ciphertext: str)
-> plaintext
-> nil, error
```
AES GCM accepts 16 and 32 byte keys
ChaCha20 Poly1305 uses a 32 byte key and a 12 byte nonce
A 16 byte tag is appended to ciphertext. Decryption returns nothing when the
tag is invalid
Never use the same nonce twice with the same key
## Other functions
```lua
crypto.random_bytes(length: int) -> str
crypto.constant_time_equal(left: str, right: str) -> bool
crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str
crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str
crypto.pbkdf2(hash: str, password: str, salt: str,
iterations: int, length: int) -> str
crypto.scrypt(password: str, salt: str, n: int, r: int, p: int,
length: int, [optional]max_memory: int=0) -> str
crypto.features() -> table
```
Use scrypt or PBKDF2 with a random salt for passwords. A regular SHA256 hash is
not suitable for password storage
`features` returns the OpenSSL version and available functions
An invalid signature returns `false, "invalid_signature"`
An invalid encryption tag returns `nil, "authentication_failed"`
Invalid arguments raise a regular Lua error

View file

@ -15,6 +15,7 @@
- [bjson, json, toml, yaml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md)
- [byteutil](scripting/builtins/libbyteutil.md)
- [crypto](scripting/builtins/libcrypto.md)
- [cameras](scripting/builtins/libcameras.md)
- [ctypes](scripting/builtins/libctypes.md)
- [entities](scripting/builtins/libentities.md)

View file

@ -0,0 +1,159 @@
# Библиотека *crypto*
Библиотека предоставляет основные криптографические функции для модов
TLS и HTTP в нее не входят. Эти протоколы используют crypto для тяжелых
вычислений
Все бинарные значения передаются обычными Lua строками. Это касается ключей,
подписей, хешей и зашифрованных данных. Строки могут содержать нулевые байты
Текущая версия API находится в `crypto.API_VERSION`
## Хеширование
```lua
crypto.sha256(data: str) -> str
crypto.sha384(data: str) -> str
crypto.sha512(data: str) -> str
crypto.md5(data: str) -> str
crypto.hash(hash: str, data: str) -> str
crypto.hmac(hash: str, key: str, data: str) -> str
```
Поддерживаются `SHA256`, `SHA384`, `SHA512` и `MD5`
MD5 нужен для совместимости со старыми форматами. Для новых данных лучше
использовать SHA256
Большой файл можно передавать частями
```lua
local hash = crypto.hash_new("SHA256")
hash:update(part1)
hash:update(part2)
local result = hash:final()
```
После `final` контекст можно очистить через `reset`
## Подписи
```lua
crypto.ed25519_keypair() -> private_key, public_key
crypto.ed25519_public(private_key: str) -> str
crypto.ed25519_sign(private_key: str, message: str) -> str
crypto.ed25519_verify(public_key: str, message: str, signature: str)
-> true
-> false, error
```
Ключи Ed25519 имеют длину 32 байта. Подпись имеет длину 64 байта
```lua
crypto.ecdsa_keypair(curve: str) -> private_key, public_key
crypto.ecdsa_public(curve: str, private_key: str) -> str
crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str
crypto.ecdsa_verify(curve: str, public_key: str, message: str,
signature: str, hash: str)
-> true
-> false, error
```
Доступны кривые `P-256`, `P-384` и `P-521`
Публичный ключ ECDSA записывается как несжатая SEC1 точка. Подпись хранится в
DER формате
```lua
crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str,
message: str, signature: str)
-> true
-> false, error
crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str,
message: str, signature: str, salt_length: int)
-> true
-> false, error
```
Модуль и экспонента RSA передаются в big endian. Значение `salt_length = -1`
использует размер хеша
Генерации RSA ключей в библиотеке нет. Для новых ключей проще использовать
Ed25519
## Обмен ключами
```lua
crypto.x25519_keypair() -> private_key, public_key
crypto.x25519_public(private_key: str) -> str
crypto.x25519(private_key: str, peer_public_key: str) -> str
crypto.x25519_shared(private_key: str, peer_public_key: str) -> str
crypto.p256_keypair() -> private_key, public_key
crypto.p256_public(private_key: str) -> str
crypto.p256_shared(private_key: str, peer_public_key: str) -> str
```
`x25519` и `x25519_shared` выполняют одну и ту же операцию
Общий секрет не стоит использовать как готовый ключ. Сначала его нужно
обработать через HKDF
## Шифрование
```lua
crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str
crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str)
-> plaintext
-> nil, error
crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str,
plaintext: str) -> str
crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str,
ciphertext: str)
-> plaintext
-> nil, error
```
AES GCM принимает ключи размером 16 или 32 байта
ChaCha20 Poly1305 использует ключ размером 32 байта и nonce размером 12 байт
В конец зашифрованных данных добавляется тег размером 16 байт. При неверном
теге функция расшифровки ничего не возвращает
Нельзя повторно использовать один nonce с тем же ключом
## Остальные функции
```lua
crypto.random_bytes(length: int) -> str
crypto.constant_time_equal(left: str, right: str) -> bool
crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str
crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str
crypto.pbkdf2(hash: str, password: str, salt: str,
iterations: int, length: int) -> str
crypto.scrypt(password: str, salt: str, n: int, r: int, p: int,
length: int, [опционально]max_memory: int=0) -> str
crypto.features() -> table
```
Для хранения паролей следует использовать scrypt или PBKDF2 со случайной
солью. Обычный SHA256 для паролей не подходит
`features` возвращает версию OpenSSL и список доступных возможностей. Это
можно использовать если мод запускается на разных сборках движка
Неверная подпись возвращает `false, "invalid_signature"`
Неверный тег при расшифровке возвращает `nil, "authentication_failed"`
Ошибки в аргументах вызывают обычную Lua ошибку

View file

@ -37,6 +37,7 @@
openal
luajit
curl
openssl
entt
mesa
freeglut

View file

@ -18,6 +18,7 @@ endif()
find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED)
find_package(CURL REQUIRED)
find_package(OpenSSL 3.0 REQUIRED COMPONENTS Crypto)
find_package(glfw3 REQUIRED)
find_package(Freetype REQUIRED)
if(NOT APPLE)
@ -79,12 +80,13 @@ target_link_libraries(
ZLIB::ZLIB
PNG::PNG
CURL::libcurl
OpenSSL::Crypto
OpenAL::OpenAL
Vorbis::vorbis
Vorbis::vorbisfile
luajit::luajit
Freetype::Freetype
PUBLIC glm::glm # Need public for src/delegates.hpp, which including to
PUBLIC luajit::luajit
glm::glm # Need public for src/delegates.hpp, which including to
# main.cpp
)

1257
src/crypto/Crypto.cpp Normal file

File diff suppressed because it is too large Load diff

185
src/crypto/Crypto.hpp Normal file
View file

@ -0,0 +1,185 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace crypto {
constexpr int API_VERSION = 1;
using Bytes = std::vector<std::uint8_t>;
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> 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();
}

View file

@ -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);

View file

@ -0,0 +1,540 @@
#include <climits>
#include <cstdint>
#include <new>
#include <stdexcept>
#include <string>
#include <string_view>
#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 <typename Operation>
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 <typename Operation>
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<lua::Number>(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<std::size_t>(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<lua::Number>(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<int>(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<crypto::HashContext*>(
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<l_hash_context_gc>);
lua::setfield(L, "__gc");
lua::pushcfunction(L, lua::wrap<l_hash_context_update>);
lua::setfield(L, "update");
lua::pushcfunction(L, lua::wrap<l_hash_context_final>);
lua::setfield(L, "final");
lua::pushcfunction(L, lua::wrap<l_hash_context_reset>);
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<std::uint32_t>(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<l_sha256>},
{"sha384", lua::wrap<l_sha384>},
{"sha512", lua::wrap<l_sha512>},
{"md5", lua::wrap<l_md5>},
{"hash", lua::wrap<l_hash>},
{"hash_new", lua::wrap<l_hash_new>},
{"hmac", lua::wrap<l_hmac>},
{"ed25519_keypair", lua::wrap<l_ed25519_keypair>},
{"ed25519_public", lua::wrap<l_ed25519_public>},
{"ed25519_sign", lua::wrap<l_ed25519_sign>},
{"ed25519_verify", lua::wrap<l_ed25519_verify>},
{"ecdsa_keypair", lua::wrap<l_ecdsa_keypair>},
{"ecdsa_public", lua::wrap<l_ecdsa_public>},
{"ecdsa_sign", lua::wrap<l_ecdsa_sign>},
{"ecdsa_verify", lua::wrap<l_ecdsa_verify>},
{"rsa_pkcs1_verify", lua::wrap<l_rsa_pkcs1_verify>},
{"rsa_pss_verify", lua::wrap<l_rsa_pss_verify>},
{"x25519_keypair", lua::wrap<l_x25519_keypair>},
{"x25519_public", lua::wrap<l_x25519_public>},
{"x25519", lua::wrap<l_x25519>},
{"x25519_shared", lua::wrap<l_x25519>},
{"p256_keypair", lua::wrap<l_p256_keypair>},
{"p256_public", lua::wrap<l_p256_public>},
{"p256_shared", lua::wrap<l_p256_shared>},
{"aes_gcm_encrypt", lua::wrap<l_aes_gcm_encrypt>},
{"aes_gcm_decrypt", lua::wrap<l_aes_gcm_decrypt>},
{"chacha20_poly1305_encrypt", lua::wrap<l_chacha20_poly1305_encrypt>},
{"chacha20_poly1305_decrypt", lua::wrap<l_chacha20_poly1305_decrypt>},
{"random_bytes", lua::wrap<l_random_bytes>},
{"constant_time_equal", lua::wrap<l_constant_time_equal>},
{"hkdf_extract", lua::wrap<l_hkdf_extract>},
{"hkdf_expand", lua::wrap<l_hkdf_expand>},
{"pbkdf2", lua::wrap<l_pbkdf2>},
{"scrypt", lua::wrap<l_scrypt>},
{"features", lua::wrap<l_features>},
{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);
}
}

View file

@ -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);

470
test/crypto/Crypto.cpp Normal file
View file

@ -0,0 +1,470 @@
#include "crypto/Crypto.hpp"
#include <gtest/gtest.h>
#include <openssl/bn.h>
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <memory>
#include <string>
namespace {
using PkeyHandle = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
using MdCtxHandle = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>;
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::uint8_t>(
std::stoul(value.substr(i, 2), nullptr, 16)
)
);
}
return result;
}
std::string binary(const crypto::Bytes& value) {
return {reinterpret_cast<const char*>(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<BIGNUM, decltype(&BN_free)> value(raw, BN_free);
crypto::Bytes result(
static_cast<std::size_t>(BN_num_bytes(value.get()))
);
BN_bn2bin(value.get(), result.data());
return result;
}
}
TEST(Crypto, HashesAndHmacMatchKnownVectors) {
EXPECT_EQ(
crypto::digest("SHA256", "abc"),
hex("ba7816bf8f01cfea414140de5dae2223"
"b00361a396177a9cb410ff61f20015ad")
);
EXPECT_EQ(
crypto::digest("SHA384", "abc"),
hex("cb00753f45a35e8bb5a03d699ac65007"
"272c32ab0eded1631a8b605a43ff5bed"
"8086072ba1e7cc2358baeca134c825a7")
);
EXPECT_EQ(
crypto::digest("SHA512", "abc"),
hex("ddaf35a193617abacc417349ae204131"
"12e6fa4e89a97ea20a9eeee64b55d39"
"a2192992a274fc1a836ba3c23a3feebbd"
"454d4423643ce80e2a9ac94fa54ca49f")
);
EXPECT_EQ(
crypto::digest("MD5", "abc"), hex("900150983cd24fb0d6963f7d28e17f72")
);
const crypto::Bytes key(20, 0x0b);
EXPECT_EQ(
crypto::hmac("SHA256", binary(key), "Hi There"),
hex("b0344c61d8db38535ca8afceaf0bf12b"
"881dc200c9833da726e9376c2e32cff7")
);
}
TEST(Crypto, StreamingHashCanBeResetAndRejectsUpdateAfterFinal) {
crypto::HashContext context("SHA256");
context.update("a");
context.update(std::string("b\0c", 3));
EXPECT_EQ(
context.final(), crypto::digest("SHA256", std::string("ab\0c", 4))
);
try {
context.update("more");
FAIL() << "update after final must fail";
} catch (const crypto::Error& error) {
EXPECT_EQ(error.code(), crypto::ErrorCode::InvalidState);
}
context.reset();
context.update("abc");
EXPECT_EQ(context.final(), crypto::digest("SHA256", "abc"));
}
TEST(Crypto, Ed25519MatchesRfc8032Vector) {
auto publicKey =
hex("d75a980182b10ab7d54bfed3c964073a"
"0ee172f3daa62325af021a68f707511a");
auto signature =
hex("e5564300c360ac729086e2cc806e828a"
"84877f1eb8e5d974d873e06522490155"
"5fb8821590a33bacc61e39701cf9b46b"
"d25bf5f0595bbe24655141438e7a100b");
EXPECT_TRUE(
crypto::ed25519Verify(binary(publicKey), "", binary(signature))
);
signature[0] ^= 1;
EXPECT_FALSE(
crypto::ed25519Verify(binary(publicKey), "", binary(signature))
);
}
TEST(Crypto, Ed25519GeneratesKeysAndSigns) {
const auto keyPair = crypto::ed25519KeyPair();
ASSERT_EQ(keyPair.privateKey.size(), std::size_t {32});
ASSERT_EQ(keyPair.publicKey.size(), std::size_t {32});
EXPECT_EQ(
crypto::ed25519Public(binary(keyPair.privateKey)), keyPair.publicKey
);
const auto signature =
crypto::ed25519Sign(binary(keyPair.privateKey), std::string("a\0b", 3));
EXPECT_TRUE(
crypto::ed25519Verify(
binary(keyPair.publicKey), std::string("a\0b", 3), binary(signature)
)
);
}
TEST(Crypto, EcdsaVerifiesAllSupportedCurves) {
struct Case {
const char* curve;
const char* opensslCurve;
const EVP_MD* digest;
const char* hash;
};
const Case cases[] {
{"P-256", "prime256v1", EVP_sha256(), "SHA256"},
{"P-384", "secp384r1", EVP_sha384(), "SHA384"},
{"P-521", "secp521r1", EVP_sha512(), "SHA512"}
};
for (const auto& item : cases) {
SCOPED_TRACE(item.curve);
PkeyHandle key(
EVP_PKEY_Q_keygen(nullptr, nullptr, "EC", item.opensslCurve),
EVP_PKEY_free
);
ASSERT_NE(key, nullptr);
std::size_t publicKeySize = 0;
ASSERT_EQ(
EVP_PKEY_get_octet_string_param(
key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicKeySize
),
1
);
crypto::Bytes publicKey(publicKeySize);
ASSERT_EQ(
EVP_PKEY_get_octet_string_param(
key.get(),
OSSL_PKEY_PARAM_PUB_KEY,
publicKey.data(),
publicKey.size(),
&publicKeySize
),
1
);
auto signature = sign(key.get(), item.digest, "message");
EXPECT_TRUE(
crypto::ecdsaVerify(
item.curve,
binary(publicKey),
"message",
binary(signature),
item.hash
)
);
EXPECT_FALSE(
crypto::ecdsaVerify(
item.curve,
binary(publicKey),
"tampered",
binary(signature),
item.hash
)
);
}
}
TEST(Crypto, EcdsaGeneratesKeysAndSignsOnAllSupportedCurves) {
struct Case {
const char* curve;
const char* hash;
std::size_t privateSize;
std::size_t publicSize;
};
const Case cases[] {
{"P-256", "SHA256", 32, 65},
{"P-384", "SHA384", 48, 97},
{"P-521", "SHA512", 66, 133}
};
for (const auto& item : cases) {
SCOPED_TRACE(item.curve);
const auto keyPair = crypto::ecdsaKeyPair(item.curve);
EXPECT_EQ(keyPair.privateKey.size(), item.privateSize);
EXPECT_EQ(keyPair.publicKey.size(), item.publicSize);
EXPECT_EQ(
crypto::ecdsaPublic(item.curve, binary(keyPair.privateKey)),
keyPair.publicKey
);
const auto signature = crypto::ecdsaSign(
item.curve, binary(keyPair.privateKey), "message", item.hash
);
EXPECT_TRUE(
crypto::ecdsaVerify(
item.curve,
binary(keyPair.publicKey),
"message",
binary(signature),
item.hash
)
);
}
}
TEST(Crypto, RsaVerifiesPkcs1AndPssSignatures) {
PkeyHandle key(
EVP_PKEY_Q_keygen(nullptr, nullptr, "RSA", 2048), EVP_PKEY_free
);
ASSERT_NE(key, nullptr);
const auto modulus = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_N);
const auto exponent = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_E);
auto pkcs1 = sign(key.get(), EVP_sha256(), "message", RSA_PKCS1_PADDING);
EXPECT_TRUE(
crypto::rsaPkcs1Verify(
"SHA256",
binary(modulus),
binary(exponent),
"message",
binary(pkcs1)
)
);
EXPECT_FALSE(
crypto::rsaPkcs1Verify(
"SHA256",
binary(modulus),
binary(exponent),
"tampered",
binary(pkcs1)
)
);
auto pss = sign(
key.get(),
EVP_sha256(),
"message",
RSA_PKCS1_PSS_PADDING,
RSA_PSS_SALTLEN_DIGEST
);
EXPECT_TRUE(
crypto::rsaPssVerify(
"SHA256",
binary(modulus),
binary(exponent),
"message",
binary(pss),
-1
)
);
EXPECT_FALSE(
crypto::rsaPssVerify(
"SHA256",
binary(modulus),
binary(exponent),
"tampered",
binary(pss),
-1
)
);
}
TEST(Crypto, X25519MatchesRfc7748PublicKeyVector) {
const auto privateKey =
hex("77076d0a7318a57d3c16c17251b26645"
"df4c2f87ebc0992ab177fba51db92c2a");
EXPECT_EQ(
crypto::x25519Public(binary(privateKey)),
hex("8520f0098930a754748b7ddcb43ef75a0"
"dbf3a0d26381af4eba4a98eaa9b4e6a")
);
}
TEST(Crypto, X25519GeneratedKeyPairsDeriveTheSameSecret) {
const auto alice = crypto::x25519KeyPair();
const auto bob = crypto::x25519KeyPair();
EXPECT_EQ(alice.privateKey.size(), std::size_t {32});
EXPECT_EQ(alice.publicKey.size(), std::size_t {32});
EXPECT_EQ(crypto::x25519Public(binary(alice.privateKey)), alice.publicKey);
EXPECT_EQ(
crypto::x25519(binary(alice.privateKey), binary(bob.publicKey)),
crypto::x25519(binary(bob.privateKey), binary(alice.publicKey))
);
}
TEST(Crypto, P256DerivesPublicKeyAndSharedSecret) {
crypto::Bytes privateA(32, 0);
crypto::Bytes privateB(32, 0);
privateA.back() = 1;
privateB.back() = 2;
const auto publicA = crypto::p256Public(binary(privateA));
const auto publicB = crypto::p256Public(binary(privateB));
EXPECT_EQ(
publicA,
hex("046b17d1f2e12c4247f8bce6e563a440"
"f277037d812deb33a0f4a13945d898c296"
"4fe342e2fe1a7f9b8ee7eb4a7c0f9e16"
"2bce33576b315ececbb6406837bf51f5")
);
EXPECT_EQ(
crypto::p256Shared(binary(privateA), binary(publicB)),
crypto::p256Shared(binary(privateB), binary(publicA))
);
}
TEST(Crypto, AesGcmMatchesKnownVectorAndRejectsTampering) {
const crypto::Bytes key(16, 0);
const crypto::Bytes nonce(12, 0);
const crypto::Bytes plaintext(16, 0);
auto ciphertext = crypto::aesGcmEncrypt(
binary(key), binary(nonce), "", binary(plaintext)
);
EXPECT_EQ(
ciphertext,
hex("0388dace60b6a392f328c2b971b2fe78"
"ab6e47d42cec13bdf53a67b21257bddf")
);
EXPECT_EQ(
crypto::aesGcmDecrypt(
binary(key), binary(nonce), "", binary(ciphertext)
),
plaintext
);
ciphertext.back() ^= 1;
EXPECT_THROW(
crypto::aesGcmDecrypt(
binary(key), binary(nonce), "", binary(ciphertext)
),
std::runtime_error
);
}
TEST(Crypto, Chacha20Poly1305RoundTripsAndRejectsTampering) {
const crypto::Bytes key(32, 0);
const crypto::Bytes nonce(12, 0);
auto ciphertext = crypto::chacha20Poly1305Encrypt(
binary(key), binary(nonce), "aad", "plaintext"
);
EXPECT_EQ(
binary(
crypto::chacha20Poly1305Decrypt(
binary(key), binary(nonce), "aad", binary(ciphertext)
)
),
"plaintext"
);
ciphertext.back() ^= 1;
EXPECT_THROW(
crypto::chacha20Poly1305Decrypt(
binary(key), binary(nonce), "aad", binary(ciphertext)
),
std::runtime_error
);
}
TEST(Crypto, HkdfMatchesRfc5869Vector) {
const crypto::Bytes ikm(22, 0x0b);
const auto salt = hex("000102030405060708090a0b0c");
const auto info = hex("f0f1f2f3f4f5f6f7f8f9");
const auto prk = crypto::hkdfExtract("SHA256", binary(salt), binary(ikm));
EXPECT_EQ(
prk,
hex("077709362c2e32df0ddc3f0dc47bba63"
"90b6c73bb50f9c3122ec844ad7c2b3e5")
);
EXPECT_EQ(
crypto::hkdfExpand("SHA256", binary(prk), binary(info), 42),
hex("3cb25f25faacd57a90434f64d0362f2a"
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
"34007208d5b887185865")
);
}
TEST(Crypto, PasswordKdfsMatchKnownVectors) {
EXPECT_EQ(
crypto::pbkdf2("SHA256", "password", "salt", 1, 32),
hex("120fb6cffcf8b32c43e7225256c4f837"
"a86548c92ccc35480805987cb70be17b")
);
EXPECT_EQ(
crypto::scrypt("", "", 16, 1, 1, 64),
hex("77d6576238657b203b19ca42c18a0497"
"f16b4844e3074ae8dfdffa3fede21442f"
"cd0069ded0948f8326a753a0fc81f17e"
"8d3e0fb2e0d3628cf35e20c38d18906")
);
}
TEST(Crypto, ReportsRuntimeCapabilitiesAndStableErrors) {
const auto available = crypto::features();
EXPECT_TRUE(available.sha256);
EXPECT_TRUE(available.ed25519);
EXPECT_TRUE(available.ecdsa);
EXPECT_TRUE(available.x25519);
EXPECT_TRUE(available.aes128Gcm);
EXPECT_TRUE(available.pbkdf2);
EXPECT_TRUE(available.scrypt);
EXPECT_EQ(crypto::API_VERSION, 1);
const crypto::Bytes key(16, 0);
const crypto::Bytes nonce(12, 0);
auto ciphertext =
crypto::aesGcmEncrypt(binary(key), binary(nonce), "", "secret");
ciphertext.back() ^= 1;
try {
crypto::aesGcmDecrypt(
binary(key), binary(nonce), "", binary(ciphertext)
);
FAIL() << "tampered ciphertext must fail";
} catch (const crypto::Error& error) {
EXPECT_EQ(error.code(), crypto::ErrorCode::AuthenticationFailed);
EXPECT_STREQ(error.codeName(), "authentication_failed");
}
}
TEST(Crypto, UtilitiesHandleBinaryData) {
const std::string binaryValue("a\0b", 3);
EXPECT_TRUE(crypto::constantTimeEqual(binaryValue, binaryValue));
EXPECT_FALSE(crypto::constantTimeEqual(binaryValue, "a"));
EXPECT_FALSE(crypto::constantTimeEqual("left", "lest"));
EXPECT_EQ(crypto::randomBytes(32).size(), std::size_t {32});
}

70
test/crypto/CryptoLua.cpp Normal file
View file

@ -0,0 +1,70 @@
#include <gtest/gtest.h>
#include <memory>
#include "logic/scripting/lua/libs/api_lua.hpp"
namespace {
struct LuaCloser {
void operator()(lua::State* state) const {
lua_close(state);
}
};
}
TEST(CryptoLua, PublicV1ApiWorksFromLua) {
std::unique_ptr<lua::State, LuaCloser> state(luaL_newstate());
ASSERT_NE(state, nullptr);
luaL_openlibs(state.get());
lua::openlib(state.get(), "crypto", cryptolib);
initialize_cryptolib(state.get());
const char* script = R"lua(
assert(crypto.API_VERSION == 1)
local features = crypto.features()
assert(features.api_version == 1)
assert(features.backend == "OpenSSL")
local binary = "a\0b"
assert(crypto.hash("SHA256", binary) == crypto.sha256(binary))
local stream = crypto.hash_new("SHA256")
assert(stream:update("a"):update("\0b"):final() == crypto.sha256(binary))
stream:reset():update("abc")
assert(stream:final() == crypto.sha256("abc"))
local private, public = crypto.ed25519_keypair()
assert(crypto.ed25519_public(private) == public)
local signature = crypto.ed25519_sign(private, binary)
assert(crypto.ed25519_verify(public, binary, signature) == true)
local ok, error_code = crypto.ed25519_verify(public, "changed", signature)
assert(ok == false and error_code == "invalid_signature")
local x_private_a, x_public_a = crypto.x25519_keypair()
local x_private_b, x_public_b = crypto.x25519_keypair()
assert(crypto.x25519(x_private_a, x_public_b) ==
crypto.x25519_shared(x_private_b, x_public_a))
local p_private_a, p_public_a = crypto.p256_keypair()
local p_private_b, p_public_b = crypto.p256_keypair()
assert(crypto.p256_public(p_private_a) == p_public_a)
assert(crypto.p256_shared(p_private_a, p_public_b) ==
crypto.p256_shared(p_private_b, p_public_a))
local key = crypto.random_bytes(16)
local nonce = crypto.random_bytes(12)
local ciphertext = crypto.aes_gcm_encrypt(key, nonce, "aad", binary)
assert(crypto.aes_gcm_decrypt(key, nonce, "aad", ciphertext) == binary)
local plaintext, decrypt_error = crypto.aes_gcm_decrypt(
key, nonce, "changed", ciphertext
)
assert(plaintext == nil and decrypt_error == "authentication_failed")
assert(#crypto.pbkdf2("SHA256", "password", "salt", 1, 32) == 32)
assert(#crypto.scrypt("password", "salt", 16, 1, 1, 32) == 32)
)lua";
const int result = luaL_dostring(state.get(), script);
if (result != 0) {
FAIL() << lua_tostring(state.get(), -1);
}
}

View file

@ -14,6 +14,7 @@
"entt",
"gtest",
"curl",
"freetype"
"freetype",
"openssl"
]
}