diff --git a/SANDBOX_MILESTONES.md b/SANDBOX_MILESTONES.md index 56c97f0..5f27448 100644 --- a/SANDBOX_MILESTONES.md +++ b/SANDBOX_MILESTONES.md @@ -202,8 +202,9 @@ TEST(SafeRequire, CachesModules); --- -## Milestone 5: Timer & Callback System +## Milestone 5: Timer & Callback System ✅ +**Status**: Complete **Goal**: Safe timer APIs managed by kernel. **Estimated Files**: 1 new file @@ -251,8 +252,9 @@ TEST(TimerManager, CleansUpOnStop); --- -## Milestone 6: JSON & Crypto APIs +## Milestone 6: JSON & Crypto APIs ✅ +**Status**: Complete **Goal**: Safe data parsing and cryptographic primitives. **Estimated Files**: 2 new files diff --git a/SANDBOX_MILESTONE_6.md b/SANDBOX_MILESTONE_6.md new file mode 100644 index 0000000..d5ed2c6 --- /dev/null +++ b/SANDBOX_MILESTONE_6.md @@ -0,0 +1,411 @@ +# Milestone 6: JSON & Crypto APIs + +**Status**: ✅ Complete +**Goal**: Safe data parsing and cryptographic primitives. + +--- + +## Overview + +This milestone implements secure JSON encoding/decoding and cryptographic primitives for Lua apps: +- JSON API with depth/size limits to prevent DoS attacks +- Crypto API with secure random, hashing, and HMAC +- Replace insecure `math.random` with per-app CSPRNG + +### Key Deliverables + +1. **JSON API** - Safe encode/decode with limits +2. **Crypto API** - Hash, HMAC, random bytes +3. **Secure math.random replacement** + +--- + +## File Structure + +``` +src/main/cpp/sandbox/ +├── json_api.h # NEW - JSON API header +├── json_api.cpp # NEW - JSON implementation +├── crypto_api.h # NEW - Crypto API header +└── crypto_api.cpp # NEW - Crypto implementation +``` + +--- + +## Implementation Details + +### 1. JSON API + +```cpp +// json_api.h +#pragma once + +#include + +struct lua_State; + +namespace mosis { + +// Configuration limits +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 +``` + +#### json.decode(str) -> table + +- Parse JSON string to Lua table +- Enforce depth limit (32 levels) +- Enforce string length limit (1 MB) +- Enforce array/object size limits +- Return `nil, error_message` on failure + +#### json.encode(table) -> string + +- Convert Lua table to JSON string +- Detect cycles (error on circular references) +- Enforce output size limit (10 MB) +- Handle Lua types: nil, boolean, number, string, table +- Error on unsupported types (functions, userdata, threads) + +### 2. Crypto API + +```cpp +// crypto_api.h +#pragma once + +#include +#include +#include + +struct lua_State; + +namespace mosis { + +// Per-app cryptographically secure RNG +class SecureRandom { +public: + SecureRandom(); + + // Get random bytes + 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; +}; + +// Register crypto.* APIs as globals +void RegisterCryptoAPI(lua_State* L); + +// Register secure math.random replacement +void RegisterSecureMathRandom(lua_State* L, SecureRandom* rng); + +} // namespace mosis +``` + +#### crypto.randomBytes(n) -> string + +- Generate `n` cryptographically secure random bytes +- Limit: max 1024 bytes per call +- Use system CSPRNG (std::random_device or platform-specific) + +#### crypto.hash(algorithm, data) -> string + +- Supported algorithms: "sha256", "sha512", "sha1", "md5" +- Returns hex-encoded hash +- Input size limit: 10 MB + +#### crypto.hmac(algorithm, key, data) -> string + +- HMAC using specified algorithm +- Returns hex-encoded result +- Key and data limits same as hash + +### 3. Secure math.random + +Replace Lua's `math.random` and remove `math.randomseed`: + +```lua +-- After registration: +math.random() -- Returns double in [0.0, 1.0) using CSPRNG +math.random(n) -- Returns integer in [1, n] +math.random(m, n) -- Returns integer in [m, n] +math.randomseed -- Removed (nil) +``` + +--- + +## Test Cases + +### Test 1: JSON Decode Valid + +```cpp +bool Test_JsonDecodeValid(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local obj = json.decode('{"name":"test","value":42,"arr":[1,2,3]}') + assert(obj.name == "test") + assert(obj.value == 42) + assert(#obj.arr == 3) + )"; + + if (!sandbox.LoadString(script, "decode_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 2: JSON Decode Rejects Deep Nesting + +```cpp +bool Test_JsonDecodeRejectsDeep(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::JsonLimits limits; + limits.max_depth = 5; + mosis::RegisterJsonAPI(sandbox.GetState(), limits); + + // Create deeply nested JSON (10 levels) + std::string deep_json = "[[[[[[[[[[1]]]]]]]]]]"; + + std::string script = + "local result, err = json.decode('" + deep_json + "')\n" + "assert(result == nil, 'should fail')\n" + "assert(err:find('depth'), 'should mention depth')"; + + if (!sandbox.LoadString(script, "deep_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 3: JSON Encode Detects Cycles + +```cpp +bool Test_JsonEncodeDetectsCycles(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local t = {a = 1} + t.self = t -- Create cycle + local result, err = json.encode(t) + assert(result == nil, 'should fail on cycle') + assert(err:find('cycle') or err:find('circular'), 'should mention cycle') + )"; + + if (!sandbox.LoadString(script, "cycle_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 4: JSON Encode Valid + +```cpp +bool Test_JsonEncodeValid(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local str = json.encode({name = "test", value = 42, arr = {1, 2, 3}}) + assert(type(str) == "string") + -- Decode back to verify + local obj = json.decode(str) + assert(obj.name == "test") + )"; + + if (!sandbox.LoadString(script, "encode_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 5: Crypto RandomBytes + +```cpp +bool Test_CryptoRandomBytes(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local bytes = crypto.randomBytes(16) + assert(#bytes == 16, 'should be 16 bytes') + + -- Should be different each time + local bytes2 = crypto.randomBytes(16) + assert(bytes ~= bytes2, 'should be random') + )"; + + if (!sandbox.LoadString(script, "random_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 6: Crypto Hash SHA256 + +```cpp +bool Test_CryptoHashSHA256(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local hash = crypto.hash("sha256", "hello") + -- Known SHA256 of "hello" + assert(hash == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824") + )"; + + if (!sandbox.LoadString(script, "hash_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 7: Crypto HMAC + +```cpp +bool Test_CryptoHMAC(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local hmac = crypto.hmac("sha256", "key", "message") + -- Known HMAC-SHA256 of "message" with key "key" + assert(hmac == "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a") + )"; + + if (!sandbox.LoadString(script, "hmac_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 8: Secure Math.Random + +```cpp +bool Test_SecureMathRandom(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::SecureRandom rng; + mosis::RegisterSecureMathRandom(sandbox.GetState(), &rng); + + std::string script = R"( + -- math.randomseed should be removed + assert(math.randomseed == nil, 'randomseed should be removed') + + -- math.random should work + local r1 = math.random() + assert(r1 >= 0 and r1 < 1, 'random() should return [0,1)') + + local r2 = math.random(10) + assert(r2 >= 1 and r2 <= 10, 'random(n) should return [1,n]') + + local r3 = math.random(5, 15) + assert(r3 >= 5 and r3 <= 15, 'random(m,n) should return [m,n]') + )"; + + if (!sandbox.LoadString(script, "math_random_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +### Test 9: JSON Rejects Too Large + +```cpp +bool Test_JsonRejectsTooLarge(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::JsonLimits limits; + limits.max_array_size = 10; + mosis::RegisterJsonAPI(sandbox.GetState(), limits); + + std::string script = R"( + -- Try to decode array with 20 elements + local result, err = json.decode('[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]') + assert(result == nil, 'should fail') + assert(err:find('size') or err:find('limit'), 'should mention size limit') + )"; + + if (!sandbox.LoadString(script, "size_test")) { + error_msg = sandbox.GetLastError(); + return false; + } + return true; +} +``` + +--- + +## Acceptance Criteria + +All tests pass: + +- [x] `Test_JsonDecodeValid` - Decodes valid JSON to Lua table +- [x] `Test_JsonDecodeRejectsDeep` - Rejects deeply nested JSON +- [x] `Test_JsonEncodeValid` - Encodes Lua table to JSON string +- [x] `Test_JsonEncodeDetectsCycles` - Detects circular references +- [x] `Test_JsonRejectsTooLarge` - Enforces size limits +- [x] `Test_CryptoRandomBytes` - Generates secure random bytes +- [x] `Test_CryptoHashSHA256` - Computes correct SHA256 hash +- [x] `Test_CryptoHMAC` - Computes correct HMAC +- [x] `Test_SecureMathRandom` - Replaces math.random securely + +--- + +## Dependencies + +- nlohmann-json (already in vcpkg for sandbox-test) +- OpenSSL or platform crypto APIs for SHA256/HMAC (or header-only implementation) + +--- + +## Next Steps + +After Milestone 6 passes: +1. Milestone 7: Virtual Filesystem +2. Milestone 8: SQLite Database diff --git a/sandbox-test/CMakeLists.txt b/sandbox-test/CMakeLists.txt index f4ae34a..3e564be 100644 --- a/sandbox-test/CMakeLists.txt +++ b/sandbox-test/CMakeLists.txt @@ -16,6 +16,8 @@ add_library(mosis-sandbox STATIC ../src/main/cpp/sandbox/rate_limiter.cpp ../src/main/cpp/sandbox/path_sandbox.cpp ../src/main/cpp/sandbox/timer_manager.cpp + ../src/main/cpp/sandbox/json_api.cpp + ../src/main/cpp/sandbox/crypto_api.cpp ) target_include_directories(mosis-sandbox PUBLIC ../src/main/cpp/sandbox @@ -23,7 +25,12 @@ target_include_directories(mosis-sandbox PUBLIC ) target_link_libraries(mosis-sandbox PUBLIC ${LUA_LIBRARIES} + nlohmann_json::nlohmann_json ) +# Windows BCrypt for crypto API +if(WIN32) + target_link_libraries(mosis-sandbox PUBLIC bcrypt) +endif() # Test executable add_executable(sandbox-test diff --git a/sandbox-test/src/main.cpp b/sandbox-test/src/main.cpp index e1a2748..9c83357 100644 --- a/sandbox-test/src/main.cpp +++ b/sandbox-test/src/main.cpp @@ -7,6 +7,8 @@ #include "rate_limiter.h" #include "path_sandbox.h" #include "timer_manager.h" +#include "json_api.h" +#include "crypto_api.h" #include #include #include @@ -874,6 +876,204 @@ bool Test_MinIntervalEnforced(std::string& error_msg) { return true; } +//============================================================================= +// MILESTONE 6: JSON & CRYPTO API TESTS +//============================================================================= + +bool Test_JsonDecodeValid(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local obj = json.decode('{"name":"test","value":42,"arr":[1,2,3]}') + assert(obj.name == "test", "name should be test") + assert(obj.value == 42, "value should be 42") + assert(#obj.arr == 3, "arr should have 3 elements") + assert(obj.arr[1] == 1, "first element should be 1") + )"; + + if (!sandbox.LoadString(script, "decode_test")) { + error_msg = "JSON decode failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_JsonDecodeRejectsDeep(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::JsonLimits limits; + limits.max_depth = 5; + mosis::RegisterJsonAPI(sandbox.GetState(), limits); + + // Create deeply nested JSON (10 levels) + std::string script = R"( + local deep_json = '[[[[[[[[[[1]]]]]]]]]]' + local result, err = json.decode(deep_json) + assert(result == nil, 'should fail on deep nesting') + assert(err and err:find('depth'), 'error should mention depth') + )"; + + if (!sandbox.LoadString(script, "deep_test")) { + error_msg = "Test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_JsonEncodeValid(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local str = json.encode({name = "test", value = 42}) + assert(type(str) == "string", "should return string") + -- Decode back to verify round-trip + local obj = json.decode(str) + assert(obj.name == "test", "round-trip name") + assert(obj.value == 42, "round-trip value") + )"; + + if (!sandbox.LoadString(script, "encode_test")) { + error_msg = "JSON encode failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_JsonEncodeDetectsCycles(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterJsonAPI(sandbox.GetState()); + + std::string script = R"( + local t = {a = 1} + t.self = t -- Create cycle + local result, err = json.encode(t) + assert(result == nil, 'should fail on cycle') + assert(err and (err:find('cycle') or err:find('circular')), 'should mention cycle') + )"; + + if (!sandbox.LoadString(script, "cycle_test")) { + error_msg = "Test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_JsonRejectsTooLarge(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::JsonLimits limits; + limits.max_array_size = 10; + mosis::RegisterJsonAPI(sandbox.GetState(), limits); + + std::string script = R"( + -- Try to decode array with 20 elements + local result, err = json.decode('[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]') + assert(result == nil, 'should fail on large array') + assert(err and (err:find('size') or err:find('limit')), 'should mention size limit') + )"; + + if (!sandbox.LoadString(script, "size_test")) { + error_msg = "Test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_CryptoRandomBytes(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local bytes = crypto.randomBytes(16) + assert(#bytes == 16, 'should be 16 bytes') + + -- Should be different each time + local bytes2 = crypto.randomBytes(16) + assert(bytes ~= bytes2, 'should be random') + )"; + + if (!sandbox.LoadString(script, "random_test")) { + error_msg = "Random bytes test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_CryptoHashSHA256(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local hash = crypto.hash("sha256", "hello") + -- Known SHA256 of "hello" + local expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + assert(hash == expected, 'SHA256 mismatch: got ' .. hash) + )"; + + if (!sandbox.LoadString(script, "hash_test")) { + error_msg = "Hash test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_CryptoHMAC(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + mosis::RegisterCryptoAPI(sandbox.GetState()); + + std::string script = R"( + local hmac = crypto.hmac("sha256", "key", "message") + -- Known HMAC-SHA256 of "message" with key "key" + local expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a" + assert(hmac == expected, 'HMAC mismatch: got ' .. hmac) + )"; + + if (!sandbox.LoadString(script, "hmac_test")) { + error_msg = "HMAC test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + +bool Test_SecureMathRandom(std::string& error_msg) { + SandboxContext ctx = TestContext(); + LuaSandbox sandbox(ctx); + + mosis::SecureRandom rng; + mosis::RegisterSecureMathRandom(sandbox.GetState(), &rng); + + std::string script = R"( + -- math.randomseed should be removed + assert(math.randomseed == nil, 'randomseed should be removed') + + -- math.random should work + local r1 = math.random() + assert(r1 >= 0 and r1 < 1, 'random() should return [0,1)') + + local r2 = math.random(10) + assert(r2 >= 1 and r2 <= 10, 'random(n) should return [1,n]') + + local r3 = math.random(5, 15) + assert(r3 >= 5 and r3 <= 15, 'random(m,n) should return [m,n]') + )"; + + if (!sandbox.LoadString(script, "math_random_test")) { + error_msg = "Math.random test failed: " + sandbox.GetLastError(); + return false; + } + return true; +} + //============================================================================= // MAIN //============================================================================= @@ -965,6 +1165,17 @@ int main(int argc, char* argv[]) { harness.AddTest("ClearAppTimersCleanup", Test_ClearAppTimersCleanup); harness.AddTest("MinIntervalEnforced", Test_MinIntervalEnforced); + // Milestone 6: JSON & Crypto APIs + harness.AddTest("JsonDecodeValid", Test_JsonDecodeValid); + harness.AddTest("JsonDecodeRejectsDeep", Test_JsonDecodeRejectsDeep); + harness.AddTest("JsonEncodeValid", Test_JsonEncodeValid); + harness.AddTest("JsonEncodeDetectsCycles", Test_JsonEncodeDetectsCycles); + harness.AddTest("JsonRejectsTooLarge", Test_JsonRejectsTooLarge); + harness.AddTest("CryptoRandomBytes", Test_CryptoRandomBytes); + harness.AddTest("CryptoHashSHA256", Test_CryptoHashSHA256); + harness.AddTest("CryptoHMAC", Test_CryptoHMAC); + harness.AddTest("SecureMathRandom", Test_SecureMathRandom); + // Run tests auto results = harness.Run(filter); diff --git a/src/main/cpp/sandbox/crypto_api.cpp b/src/main/cpp/sandbox/crypto_api.cpp new file mode 100644 index 0000000..25eb49b --- /dev/null +++ b/src/main/cpp/sandbox/crypto_api.cpp @@ -0,0 +1,393 @@ +#include "crypto_api.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#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 lock(m_mutex); + + std::string result(count, '\0'); + for (size_t i = 0; i < count; i++) { + result[i] = static_cast(m_gen() & 0xFF); + } + return result; +} + +int64_t SecureRandom::GetInt(int64_t min, int64_t max) { + std::lock_guard lock(m_mutex); + std::uniform_int_distribution dist(min, max); + return dist(m_gen); +} + +double SecureRandom::GetDouble() { + std::lock_guard lock(m_mutex); + std::uniform_real_distribution 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(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 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(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 hashBuffer(hashLength); + + status = BCryptCreateHash(hAlg, &hHash, nullptr, 0, + (PUCHAR)key.data(), static_cast(key.size()), 0); + if (!BCRYPT_SUCCESS(status)) { + BCryptCloseAlgorithmProvider(hAlg, 0); + return ""; + } + + status = BCryptHashData(hHash, (PUCHAR)data.data(), static_cast(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(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(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(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 diff --git a/src/main/cpp/sandbox/crypto_api.h b/src/main/cpp/sandbox/crypto_api.h new file mode 100644 index 0000000..a0fee8f --- /dev/null +++ b/src/main/cpp/sandbox/crypto_api.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +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 diff --git a/src/main/cpp/sandbox/json_api.cpp b/src/main/cpp/sandbox/json_api.cpp new file mode 100644 index 0000000..b635944 --- /dev/null +++ b/src/main/cpp/sandbox/json_api.cpp @@ -0,0 +1,369 @@ +#include "json_api.h" + +#include +#include +#include +#include + +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(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() ? 1 : 0); + break; + + case json::value_t::number_integer: + case json::value_t::number_unsigned: + lua_pushinteger(L, j.get()); + break; + + case json::value_t::number_float: + lua_pushnumber(L, j.get()); + break; + + case json::value_t::string: { + const std::string& s = j.get_ref(); + 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(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(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& visited, + size_t& output_size); + +static json LuaToJson(lua_State* L, int index, const JsonLimits& limits, + int depth, std::unordered_set& 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(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(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(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 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 diff --git a/src/main/cpp/sandbox/json_api.h b/src/main/cpp/sandbox/json_api.h new file mode 100644 index 0000000..5f59d94 --- /dev/null +++ b/src/main/cpp/sandbox/json_api.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +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