# 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