add JSON and crypto APIs with sandbox protection (milestone 6 complete)

This commit is contained in:
2026-01-18 14:44:51 +01:00
parent a4ecb0f132
commit be663282d7
8 changed files with 1469 additions and 2 deletions

View File

@@ -0,0 +1,393 @@
#include "crypto_api.h"
#include <lua.hpp>
#include <sstream>
#include <iomanip>
#include <cstring>
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#endif
namespace mosis {
//=============================================================================
// SECURE RANDOM
//=============================================================================
SecureRandom::SecureRandom()
: m_gen(m_rd()) {
}
std::string SecureRandom::GetBytes(size_t count) {
std::lock_guard<std::mutex> lock(m_mutex);
std::string result(count, '\0');
for (size_t i = 0; i < count; i++) {
result[i] = static_cast<char>(m_gen() & 0xFF);
}
return result;
}
int64_t SecureRandom::GetInt(int64_t min, int64_t max) {
std::lock_guard<std::mutex> lock(m_mutex);
std::uniform_int_distribution<int64_t> dist(min, max);
return dist(m_gen);
}
double SecureRandom::GetDouble() {
std::lock_guard<std::mutex> lock(m_mutex);
std::uniform_real_distribution<double> dist(0.0, 1.0);
return dist(m_gen);
}
//=============================================================================
// HASHING (Windows BCrypt)
//=============================================================================
#ifdef _WIN32
static std::string BytesToHex(const unsigned char* data, size_t len) {
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < len; i++) {
oss << std::setw(2) << static_cast<int>(data[i]);
}
return oss.str();
}
static LPCWSTR GetBCryptAlgorithm(HashAlgorithm algo) {
switch (algo) {
case HashAlgorithm::SHA256: return BCRYPT_SHA256_ALGORITHM;
case HashAlgorithm::SHA512: return BCRYPT_SHA512_ALGORITHM;
case HashAlgorithm::SHA1: return BCRYPT_SHA1_ALGORITHM;
case HashAlgorithm::MD5: return BCRYPT_MD5_ALGORITHM;
default: return BCRYPT_SHA256_ALGORITHM;
}
}
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
NTSTATUS status;
std::string result;
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr, 0);
if (!BCRYPT_SUCCESS(status)) {
return "";
}
DWORD hashLength = 0;
DWORD resultLength = 0;
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
sizeof(hashLength), &resultLength, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
std::vector<unsigned char> hashBuffer(hashLength);
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
if (BCRYPT_SUCCESS(status)) {
result = BytesToHex(hashBuffer.data(), hashLength);
}
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return result;
}
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
NTSTATUS status;
std::string result;
status = BCryptOpenAlgorithmProvider(&hAlg, GetBCryptAlgorithm(algo), nullptr,
BCRYPT_ALG_HANDLE_HMAC_FLAG);
if (!BCRYPT_SUCCESS(status)) {
return "";
}
DWORD hashLength = 0;
DWORD resultLength = 0;
status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
sizeof(hashLength), &resultLength, 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
std::vector<unsigned char> hashBuffer(hashLength);
status = BCryptCreateHash(hAlg, &hHash, nullptr, 0,
(PUCHAR)key.data(), static_cast<ULONG>(key.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast<ULONG>(data.size()), 0);
if (!BCRYPT_SUCCESS(status)) {
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return "";
}
status = BCryptFinishHash(hHash, hashBuffer.data(), hashLength, 0);
if (BCRYPT_SUCCESS(status)) {
result = BytesToHex(hashBuffer.data(), hashLength);
}
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
return result;
}
#else
// Stub implementations for non-Windows (would need OpenSSL or similar)
std::string ComputeHash(HashAlgorithm algo, const std::string& data) {
(void)algo;
(void)data;
return "";
}
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data) {
(void)algo;
(void)key;
(void)data;
return "";
}
#endif
//=============================================================================
// LUA CRYPTO API
//=============================================================================
static const char* CRYPTO_RNG_KEY = "__mosis_crypto_rng";
static SecureRandom* GetRng(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
if (lua_islightuserdata(L, -1)) {
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return rng;
}
lua_pop(L, 1);
// Create a default RNG if none registered
static SecureRandom default_rng;
return &default_rng;
}
// crypto.randomBytes(n) -> string
static int lua_crypto_randomBytes(lua_State* L) {
lua_Integer n = luaL_checkinteger(L, 1);
if (n < 0) {
return luaL_error(L, "crypto.randomBytes: count must be non-negative");
}
if (n > 1024) {
return luaL_error(L, "crypto.randomBytes: count must not exceed 1024");
}
SecureRandom* rng = GetRng(L);
std::string bytes = rng->GetBytes(static_cast<size_t>(n));
lua_pushlstring(L, bytes.data(), bytes.size());
return 1;
}
static HashAlgorithm ParseAlgorithm(const char* name) {
if (strcmp(name, "sha256") == 0) return HashAlgorithm::SHA256;
if (strcmp(name, "sha512") == 0) return HashAlgorithm::SHA512;
if (strcmp(name, "sha1") == 0) return HashAlgorithm::SHA1;
if (strcmp(name, "md5") == 0) return HashAlgorithm::MD5;
return HashAlgorithm::SHA256; // Default
}
// crypto.hash(algorithm, data) -> string
static int lua_crypto_hash(lua_State* L) {
const char* algo_name = luaL_checkstring(L, 1);
size_t data_len;
const char* data = luaL_checklstring(L, 2, &data_len);
// Limit input size
if (data_len > 10 * 1024 * 1024) {
return luaL_error(L, "crypto.hash: input too large (max 10MB)");
}
HashAlgorithm algo = ParseAlgorithm(algo_name);
std::string result = ComputeHash(algo, std::string(data, data_len));
if (result.empty()) {
return luaL_error(L, "crypto.hash: failed to compute hash");
}
lua_pushstring(L, result.c_str());
return 1;
}
// crypto.hmac(algorithm, key, data) -> string
static int lua_crypto_hmac(lua_State* L) {
const char* algo_name = luaL_checkstring(L, 1);
size_t key_len;
const char* key = luaL_checklstring(L, 2, &key_len);
size_t data_len;
const char* data = luaL_checklstring(L, 3, &data_len);
// Limit input sizes
if (key_len > 1024) {
return luaL_error(L, "crypto.hmac: key too large (max 1KB)");
}
if (data_len > 10 * 1024 * 1024) {
return luaL_error(L, "crypto.hmac: data too large (max 10MB)");
}
HashAlgorithm algo = ParseAlgorithm(algo_name);
std::string result = ComputeHMAC(algo, std::string(key, key_len),
std::string(data, data_len));
if (result.empty()) {
return luaL_error(L, "crypto.hmac: failed to compute HMAC");
}
lua_pushstring(L, result.c_str());
return 1;
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
void RegisterCryptoAPI(lua_State* L) {
// Create crypto table
lua_newtable(L);
lua_pushcfunction(L, lua_crypto_randomBytes);
lua_setfield(L, -2, "randomBytes");
lua_pushcfunction(L, lua_crypto_hash);
lua_setfield(L, -2, "hash");
lua_pushcfunction(L, lua_crypto_hmac);
lua_setfield(L, -2, "hmac");
// Set as global (bypassing proxy)
SetGlobalInRealG(L, "crypto");
}
//=============================================================================
// SECURE MATH.RANDOM
//=============================================================================
static const char* MATH_RNG_KEY = "__mosis_math_rng";
// math.random([m [, n]]) - secure version
static int lua_secure_random(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
if (!lua_islightuserdata(L, -1)) {
lua_pop(L, 1);
return luaL_error(L, "math.random: RNG not initialized");
}
SecureRandom* rng = static_cast<SecureRandom*>(lua_touserdata(L, -1));
lua_pop(L, 1);
int nargs = lua_gettop(L);
if (nargs == 0) {
// Return double in [0.0, 1.0)
lua_pushnumber(L, rng->GetDouble());
return 1;
} else if (nargs == 1) {
// Return integer in [1, n]
lua_Integer n = luaL_checkinteger(L, 1);
if (n < 1) {
return luaL_error(L, "math.random: interval is empty");
}
lua_pushinteger(L, rng->GetInt(1, n));
return 1;
} else {
// Return integer in [m, n]
lua_Integer m = luaL_checkinteger(L, 1);
lua_Integer n = luaL_checkinteger(L, 2);
if (m > n) {
return luaL_error(L, "math.random: interval is empty");
}
lua_pushinteger(L, rng->GetInt(m, n));
return 1;
}
}
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng) {
// Store RNG in registry
lua_pushlightuserdata(L, rng);
lua_setfield(L, LUA_REGISTRYINDEX, MATH_RNG_KEY);
// Also store for crypto API
lua_pushlightuserdata(L, rng);
lua_setfield(L, LUA_REGISTRYINDEX, CRYPTO_RNG_KEY);
// Get the math table
lua_getglobal(L, "math");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return;
}
// Replace math.random with secure version
lua_pushcfunction(L, lua_secure_random);
lua_setfield(L, -2, "random");
// Remove math.randomseed
lua_pushnil(L);
lua_setfield(L, -2, "randomseed");
lua_pop(L, 1); // Pop math table
}
} // namespace mosis

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <cstdint>
#include <random>
#include <mutex>
struct lua_State;
namespace mosis {
// Per-app cryptographically secure RNG
class SecureRandom {
public:
SecureRandom();
// Get random bytes as binary string
std::string GetBytes(size_t count);
// Get random integer in range [min, max]
int64_t GetInt(int64_t min, int64_t max);
// Get random double in range [0.0, 1.0)
double GetDouble();
private:
std::random_device m_rd;
std::mt19937_64 m_gen;
std::mutex m_mutex;
};
// Hash algorithms supported
enum class HashAlgorithm {
SHA256,
SHA512,
SHA1,
MD5
};
// Compute hash of data
std::string ComputeHash(HashAlgorithm algo, const std::string& data);
// Compute HMAC of data with key
std::string ComputeHMAC(HashAlgorithm algo, const std::string& key, const std::string& data);
// Register crypto.* APIs as globals
void RegisterCryptoAPI(lua_State* L);
// Register secure math.random replacement (removes math.randomseed)
void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng);
} // namespace mosis

View File

@@ -0,0 +1,369 @@
#include "json_api.h"
#include <lua.hpp>
#include <nlohmann/json.hpp>
#include <unordered_set>
#include <sstream>
using json = nlohmann::json;
namespace mosis {
// Registry key for storing limits
static const char* JSON_LIMITS_KEY = "__mosis_json_limits";
// Get limits from registry
static JsonLimits GetLimits(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
if (lua_islightuserdata(L, -1)) {
JsonLimits* limits = static_cast<JsonLimits*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return *limits;
}
lua_pop(L, 1);
return JsonLimits{};
}
//=============================================================================
// JSON DECODE
//=============================================================================
// Custom exception for JSON errors (thrown instead of luaL_error to allow catching)
class JsonError : public std::runtime_error {
public:
explicit JsonError(const std::string& msg) : std::runtime_error(msg) {}
};
// Forward declaration
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth);
static void JsonToLua(lua_State* L, const json& j, const JsonLimits& limits, int depth) {
if (depth > limits.max_depth) {
throw JsonError("maximum depth exceeded");
}
switch (j.type()) {
case json::value_t::null:
lua_pushnil(L);
break;
case json::value_t::boolean:
lua_pushboolean(L, j.get<bool>() ? 1 : 0);
break;
case json::value_t::number_integer:
case json::value_t::number_unsigned:
lua_pushinteger(L, j.get<lua_Integer>());
break;
case json::value_t::number_float:
lua_pushnumber(L, j.get<lua_Number>());
break;
case json::value_t::string: {
const std::string& s = j.get_ref<const std::string&>();
if (s.size() > limits.max_string_length) {
throw JsonError("string too large");
}
lua_pushlstring(L, s.c_str(), s.size());
break;
}
case json::value_t::array: {
if (j.size() > limits.max_array_size) {
throw JsonError("array size limit exceeded");
}
lua_createtable(L, static_cast<int>(j.size()), 0);
int i = 1;
for (const auto& elem : j) {
JsonToLua(L, elem, limits, depth + 1);
lua_rawseti(L, -2, i++);
}
break;
}
case json::value_t::object: {
if (j.size() > limits.max_object_size) {
throw JsonError("object size limit exceeded");
}
lua_createtable(L, 0, static_cast<int>(j.size()));
for (auto it = j.begin(); it != j.end(); ++it) {
if (it.key().size() > limits.max_string_length) {
throw JsonError("key too large");
}
lua_pushlstring(L, it.key().c_str(), it.key().size());
JsonToLua(L, it.value(), limits, depth + 1);
lua_rawset(L, -3);
}
break;
}
default:
lua_pushnil(L);
break;
}
}
// json.decode(str) -> table|nil, error
static int lua_json_decode(lua_State* L) {
size_t len;
const char* str = luaL_checklstring(L, 1, &len);
JsonLimits limits = GetLimits(L);
if (len > limits.max_output_size) {
lua_pushnil(L);
lua_pushstring(L, "input too large");
return 2;
}
try {
json j = json::parse(str, str + len);
JsonToLua(L, j, limits, 0);
return 1;
} catch (const JsonError& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const json::parse_error& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const std::exception& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
}
}
//=============================================================================
// JSON ENCODE
//=============================================================================
// Forward declaration
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
int depth, std::unordered_set<const void*>& visited,
size_t& output_size);
static json LuaToJson(lua_State* L, int index, const JsonLimits& limits,
int depth, std::unordered_set<const void*>& visited,
size_t& output_size) {
if (depth > limits.max_depth) {
throw JsonError("maximum depth exceeded");
}
if (output_size > limits.max_output_size) {
throw JsonError("output size limit exceeded");
}
int type = lua_type(L, index);
switch (type) {
case LUA_TNIL:
return nullptr;
case LUA_TBOOLEAN:
return lua_toboolean(L, index) != 0;
case LUA_TNUMBER:
if (lua_isinteger(L, index)) {
return lua_tointeger(L, index);
}
return lua_tonumber(L, index);
case LUA_TSTRING: {
size_t len;
const char* s = lua_tolstring(L, index, &len);
if (len > limits.max_string_length) {
throw JsonError("string too large");
}
output_size += len + 2; // Approximate: string + quotes
return std::string(s, len);
}
case LUA_TTABLE: {
// Check for cycles
const void* ptr = lua_topointer(L, index);
if (visited.find(ptr) != visited.end()) {
throw JsonError("circular reference detected");
}
visited.insert(ptr);
// Determine if array or object by checking keys
bool is_array = true;
size_t array_len = 0;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) {
lua_Integer key = lua_tointeger(L, -2);
if (key >= 1) {
array_len = std::max(array_len, static_cast<size_t>(key));
} else {
is_array = false;
}
} else {
is_array = false;
}
lua_pop(L, 1);
}
// Verify array is contiguous
if (is_array && array_len > 0) {
for (size_t i = 1; i <= array_len; i++) {
lua_rawgeti(L, index, static_cast<int>(i));
if (lua_isnil(L, -1)) {
is_array = false;
}
lua_pop(L, 1);
if (!is_array) break;
}
}
if (is_array && array_len > 0) {
if (array_len > limits.max_array_size) {
throw JsonError("array size limit exceeded");
}
json arr = json::array();
for (size_t i = 1; i <= array_len; i++) {
lua_rawgeti(L, index, static_cast<int>(i));
arr.push_back(LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size));
lua_pop(L, 1);
}
visited.erase(ptr);
return arr;
} else {
// Object
json obj = json::object();
size_t key_count = 0;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
key_count++;
if (key_count > limits.max_object_size) {
throw JsonError("object size limit exceeded");
}
// Get key as string
std::string key;
if (lua_type(L, -2) == LUA_TSTRING) {
size_t len;
const char* s = lua_tolstring(L, -2, &len);
key = std::string(s, len);
} else if (lua_type(L, -2) == LUA_TNUMBER) {
if (lua_isinteger(L, -2)) {
key = std::to_string(lua_tointeger(L, -2));
} else {
key = std::to_string(lua_tonumber(L, -2));
}
} else {
lua_pop(L, 2);
throw JsonError("unsupported key type");
}
output_size += key.size() + 3; // key + quotes + colon
obj[key] = LuaToJson(L, lua_gettop(L), limits, depth + 1, visited, output_size);
lua_pop(L, 1);
}
visited.erase(ptr);
return obj;
}
}
case LUA_TFUNCTION:
case LUA_TUSERDATA:
case LUA_TTHREAD:
case LUA_TLIGHTUSERDATA:
throw JsonError(std::string("unsupported type '") + lua_typename(L, type) + "'");
default:
return nullptr;
}
}
// json.encode(table) -> string|nil, error
static int lua_json_encode(lua_State* L) {
luaL_checktype(L, 1, LUA_TTABLE);
JsonLimits limits = GetLimits(L);
std::unordered_set<const void*> visited;
size_t output_size = 0;
try {
json j = LuaToJson(L, 1, limits, 0, visited, output_size);
std::string result = j.dump();
if (result.size() > limits.max_output_size) {
lua_pushnil(L);
lua_pushstring(L, "output size limit exceeded");
return 2;
}
lua_pushlstring(L, result.c_str(), result.size());
return 1;
} catch (const JsonError& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
} catch (const std::exception& e) {
lua_pushnil(L);
lua_pushstring(L, e.what());
return 2;
}
}
//=============================================================================
// REGISTRATION
//=============================================================================
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
// Stack: value to set as global
// Get _G (might be a proxy)
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
// Check if it has a metatable with __index (proxy pattern)
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
// Found real _G through proxy's __index
// Stack: value, proxy, mt, real_G
lua_pushvalue(L, -4); // Copy value
lua_setfield(L, -2, name); // real_G[name] = value
lua_pop(L, 4); // pop real_G, mt, proxy, original value
return;
}
lua_pop(L, 2); // pop __index, metatable
}
// No proxy, set directly in _G
// Stack: value, _G
lua_pushvalue(L, -2); // Copy value
lua_setfield(L, -2, name); // _G[name] = value
lua_pop(L, 2); // pop _G, original value
}
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits) {
// Store limits in registry (allocate static storage)
static JsonLimits stored_limits;
stored_limits = limits;
lua_pushlightuserdata(L, &stored_limits);
lua_setfield(L, LUA_REGISTRYINDEX, JSON_LIMITS_KEY);
// Create json table
lua_newtable(L);
lua_pushcfunction(L, lua_json_decode);
lua_setfield(L, -2, "decode");
lua_pushcfunction(L, lua_json_encode);
lua_setfield(L, -2, "encode");
// Set as global (bypassing proxy)
SetGlobalInRealG(L, "json");
}
} // namespace mosis

View File

@@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <cstddef>
struct lua_State;
namespace mosis {
// Configuration limits for JSON operations
struct JsonLimits {
int max_depth = 32; // Maximum nesting depth
size_t max_string_length = 1 * 1024 * 1024; // 1 MB per string
size_t max_output_size = 10 * 1024 * 1024; // 10 MB total output
size_t max_array_size = 100000; // Max elements in array
size_t max_object_size = 10000; // Max keys in object
};
// Register json.encode() and json.decode() as globals
void RegisterJsonAPI(lua_State* L, const JsonLimits& limits = JsonLimits{});
} // namespace mosis