Files
MosisService/SANDBOX_MILESTONE_6.md

10 KiB

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

// json_api.h
#pragma once

#include <string>

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

// crypto_api.h
#pragma once

#include <string>
#include <cstdint>
#include <random>

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:

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

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

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

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

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

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

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

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

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

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:

  • Test_JsonDecodeValid - Decodes valid JSON to Lua table
  • Test_JsonDecodeRejectsDeep - Rejects deeply nested JSON
  • Test_JsonEncodeValid - Encodes Lua table to JSON string
  • Test_JsonEncodeDetectsCycles - Detects circular references
  • Test_JsonRejectsTooLarge - Enforces size limits
  • Test_CryptoRandomBytes - Generates secure random bytes
  • Test_CryptoHashSHA256 - Computes correct SHA256 hash
  • Test_CryptoHMAC - Computes correct HMAC
  • 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