# Mosis Lua Sandbox Security **Status**: Design Phase **Goal**: Secure app isolation with defense-in-depth approach. --- ## Overview Third-party apps run in isolated Lua environments with restricted access to system resources. Each app gets its own `lua_State` with carefully controlled APIs. ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Mosis Kernel │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ LuaSandboxManager │ │ │ │ - Creates per-app lua_State with custom allocator │ │ │ │ - Enforces memory/CPU limits │ │ │ │ - Routes permission-gated API calls │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ App A State │ │ App B State │ │ App C State │ │ │ │ (isolated) │ │ (isolated) │ │ (isolated) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ └───────────────────┴───────────────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ Permission Gate │ │ │ └────────┬────────┘ │ │ │ │ │ ┌──────────────────────────▼──────────────────────────────────┐ │ │ │ System Services │ │ │ │ Camera │ Network │ Storage │ Contacts │ Messages │ ... │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Threat Model ### Threat Categories | Category | Threat | Severity | Mitigation | |----------|--------|----------|------------| | **Code Execution** | `os.execute()`, `io.popen()` | Critical | Remove globals | | **File Access** | `io.open()`, `loadfile()` | Critical | Remove globals | | **Bytecode Injection** | `load()` with binary chunks | Critical | Text-only loading | | **Memory Exhaustion** | Infinite tables/strings | High | Custom allocator with limits | | **CPU Exhaustion** | Infinite loops | High | Instruction count hook | | **Sandbox Escape** | `debug.getregistry()` | Critical | Remove debug library | | **Global Pollution** | Modifying `_G`, `string` | Medium | Frozen globals | | **Path Traversal** | `require("../../etc/passwd")` | Critical | Path validation | | **Data Exfiltration** | Unauthorized network access | High | Permission-gated network | | **Privilege Escalation** | Access other app's data | High | Per-app storage isolation | | **Timing Attacks** | High-resolution timers | Low | Limit timer precision | | **Side Channels** | Memory/CPU usage patterns | Low | Rate limiting | ### Attacker Capabilities We assume a malicious app developer can: - Write arbitrary Lua code within the sandbox - Attempt to exploit any exposed API - Try to escape the sandbox via Lua language features - Attempt DoS via resource exhaustion - Try to access other apps' data - Attempt to exfiltrate user data --- ## Security Layers ### Layer 1: Dangerous Globals Removal **Status**: Designed Remove all dangerous functions before any app code runs. ```cpp void RemoveDangerousGlobals(lua_State* L) { const char* dangerous[] = { // Execution "dofile", "loadfile", "load", "loadstring", // Raw access (bypass metatables) "rawget", "rawset", "rawequal", "rawlen", // Metatable manipulation "getmetatable", "setmetatable", // GC manipulation "collectgarbage", // Must not exist "os", "io", "debug", "package", "require", "ffi", "jit", "newproxy", nullptr }; for (const char** p = dangerous; *p; ++p) { lua_pushnil(L); lua_setglobal(L, *p); } // Remove string.dump (creates bytecode) lua_getglobal(L, "string"); lua_pushnil(L); lua_setfield(L, -2, "dump"); lua_pop(L, 1); } ``` **Safe globals retained**: - `print` (redirected to logger) - `type`, `tonumber`, `tostring` - `pairs`, `ipairs`, `next` - `pcall`, `xpcall` - `assert`, `error` - `select`, `unpack` - `string.*` (except `dump`) - `table.*` - `math.*` - `utf8.*` ### Layer 2: Bytecode Prevention **Status**: Designed Prevent loading of binary Lua chunks which can bypass sandbox checks. ```cpp // Only allow text chunks, reject bytecode int result = luaL_loadbufferx(L, code, len, name, "t"); // mode: "t" = text only ``` Bytecode starts with `\27Lua` signature - the "t" mode rejects this. ### Layer 3: Memory Limits **Status**: Designed Custom allocator tracks and limits memory usage per app. ```cpp void* SandboxAlloc(void* ud, void* ptr, size_t osize, size_t nsize) { auto* sandbox = static_cast(ud); size_t new_usage = sandbox->m_memory_used - osize + nsize; if (nsize > 0 && new_usage > sandbox->m_limits.memory_bytes) { return nullptr; // Allocation fails } sandbox->m_memory_used = new_usage; if (nsize == 0) { free(ptr); return nullptr; } return realloc(ptr, nsize); } lua_State* L = lua_newstate(SandboxAlloc, sandbox); ``` **Default limits**: | Resource | Limit | Rationale | |----------|-------|-----------| | Memory | 16 MB | Enough for UI, prevents DoS | | String size | 1 MB | Prevent single huge allocation | | Table entries | 100,000 | Prevent hash DoS | ### Layer 4: CPU Limits **Status**: Designed Instruction count hook prevents infinite loops and excessive computation. ```cpp void InstructionHook(lua_State* L, lua_Debug* ar) { auto* sandbox = GetSandbox(L); sandbox->m_instructions += 1000; if (sandbox->m_instructions > sandbox->m_limits.instructions) { luaL_error(L, "instruction limit exceeded"); } } lua_sethook(L, InstructionHook, LUA_MASKCOUNT, 1000); ``` **Default limits**: | Context | Limit | Approx Time | |---------|-------|-------------| | Event handler | 1,000,000 | ~10ms | | Frame update | 100,000 | ~1ms | | App initialization | 10,000,000 | ~100ms | ### Layer 5: Path Sandboxing **Status**: Designed All file access restricted to app's own directory. ```cpp bool ValidatePath(const std::string& app_path, const std::string& requested) { // Reject obvious traversal attempts if (requested.find("..") != std::string::npos) return false; if (requested.find(':') != std::string::npos) return false; // Windows drive if (requested[0] == '/' || requested[0] == '\\') return false; // Absolute // Canonicalize and verify within app directory std::filesystem::path base = std::filesystem::canonical(app_path); std::filesystem::path full = std::filesystem::weakly_canonical( base / requested ); // Must be within app directory auto [base_end, full_it] = std::mismatch( base.begin(), base.end(), full.begin() ); return base_end == base.end(); } ``` ### Layer 6: Safe `require` Replacement **Status**: Designed Custom module loader only loads from app's scripts directory. ```cpp int SafeRequire(lua_State* L) { const char* module = luaL_checkstring(L, 1); auto* sandbox = GetSandbox(L); // Validate module name std::string name(module); if (!IsValidModuleName(name)) { return luaL_error(L, "invalid module name"); } // Check cache lua_getfield(L, LUA_REGISTRYINDEX, "__loaded"); lua_getfield(L, -1, module); if (!lua_isnil(L, -1)) return 1; lua_pop(L, 2); // Load from app/scripts/name.lua std::string path = sandbox->app_path + "/scripts/" + name + ".lua"; if (!ValidatePath(sandbox->app_path, "scripts/" + name + ".lua")) { return luaL_error(L, "path validation failed"); } // Load as text only if (luaL_loadfilex(L, path.c_str(), "t") != LUA_OK) { return lua_error(L); } // Execute and cache lua_call(L, 0, 1); lua_getfield(L, LUA_REGISTRYINDEX, "__loaded"); lua_pushvalue(L, -2); lua_setfield(L, -2, module); lua_pop(L, 1); return 1; } ``` ### Layer 7: Metatable Protection **Status**: Designed Prevent modification of built-in metatables and sandbox escape via metatables. ```cpp void ProtectBuiltinTables(lua_State* L) { // Protect string metatable lua_pushstring(L, ""); lua_getmetatable(L, -1); lua_pushstring(L, "string"); lua_setfield(L, -2, "__metatable"); // Prevents getmetatable lua_pop(L, 2); // Freeze _G lua_pushglobaltable(L); lua_newtable(L); lua_pushstring(L, "globals"); lua_setfield(L, -2, "__metatable"); lua_pushcfunction(L, [](lua_State* L) -> int { return luaL_error(L, "cannot modify global environment"); }); lua_setfield(L, -2, "__newindex"); lua_setmetatable(L, -2); lua_pop(L, 1); } ``` **Safe metatable access**: ```cpp // getmetatable returns __metatable field (not real metatable) // setmetatable blocked on protected tables ``` ### Layer 8: Permission-Gated APIs **Status**: Designed Sensitive APIs check permissions before execution. ```cpp class PermissionGate { public: static bool Check(lua_State* L, const std::string& permission) { auto* sandbox = GetSandbox(L); auto& perms = sandbox->m_context.permissions; return std::find(perms.begin(), perms.end(), permission) != perms.end(); } static int RequirePermission(lua_State* L, const char* perm) { if (!Check(L, perm)) { return luaL_error(L, "permission denied: %s", perm); } return 0; } }; // Usage in API int CameraCapture(lua_State* L) { PermissionGate::RequirePermission(L, "camera"); // ... actual implementation } ``` **Permission categories**: | Permission | Required For | Risk Level | |------------|--------------|------------| | `storage` | App-private file access | Normal (auto-granted) | | `camera` | Camera access | Dangerous | | `microphone` | Audio recording | Dangerous | | `location` | GPS/location | Dangerous | | `contacts.read` | Read contacts | Dangerous | | `contacts.write` | Modify contacts | Dangerous | | `network` | HTTP requests | Dangerous | | `network.websocket` | WebSocket connections | Dangerous | | `bluetooth` | Bluetooth access | Dangerous | | `system.notifications` | Show notifications | Normal | --- ## Additional Security Measures ### Network Request Sandboxing **Status**: Planned Network requests are proxied through the kernel with domain filtering. ```cpp struct NetworkPolicy { std::vector allowed_domains; // Whitelist std::vector blocked_domains; // Blacklist bool allow_localhost = false; bool allow_private_ips = false; size_t max_request_size = 10 * 1024 * 1024; // 10 MB size_t max_response_size = 50 * 1024 * 1024; // 50 MB int timeout_ms = 30000; }; class NetworkSandbox { public: bool ValidateRequest(const HttpRequest& req) { // Parse URL auto url = ParseUrl(req.url); // Block private IPs if (!m_policy.allow_private_ips && IsPrivateIP(url.host)) { return false; } // Block localhost if (!m_policy.allow_localhost && IsLocalhost(url.host)) { return false; } // Check domain whitelist/blacklist if (!m_policy.allowed_domains.empty()) { if (!MatchesDomain(url.host, m_policy.allowed_domains)) { return false; } } if (MatchesDomain(url.host, m_policy.blocked_domains)) { return false; } // Size limits if (req.body.size() > m_policy.max_request_size) { return false; } return true; } private: bool IsPrivateIP(const std::string& host) { // 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x // Also check for IPv6 private ranges } }; ``` **Manifest declaration**: ```json { "permissions": ["network"], "network": { "allowed_domains": [ "api.example.com", "*.cdn.example.com" ] } } ``` ### Timer and Callback Security **Status**: Planned Timers and callbacks are managed by the kernel, not exposed directly. ```cpp class TimerManager { public: uint32_t SetTimeout(LuaSandbox* sandbox, int callback_ref, int ms) { // Validate delay if (ms < 0) return 0; if (ms < 10) ms = 10; // Minimum 10ms granularity // Limit active timers per app if (m_app_timers[sandbox->app_id()].size() >= MAX_TIMERS_PER_APP) { return 0; } Timer timer{ .id = ++m_next_id, .sandbox = sandbox, .callback_ref = callback_ref, .fire_time = Now() + ms, .repeat = false }; m_timers.push(timer); m_app_timers[sandbox->app_id()].insert(timer.id); return timer.id; } void ClearAppTimers(const std::string& app_id) { // Called when app is stopped for (uint32_t id : m_app_timers[app_id]) { // Mark timer as cancelled } m_app_timers.erase(app_id); } private: static constexpr size_t MAX_TIMERS_PER_APP = 100; }; ``` **Lua API**: ```lua -- Safe timer APIs (kernel-managed) local id = setTimeout(function() print("fired!") end, 1000) clearTimeout(id) local id = setInterval(function() print("tick") end, 1000) clearInterval(id) ``` ### Inter-App Communication **Status**: Planned Apps communicate through kernel-mediated message passing. ```cpp struct AppMessage { std::string from_app; std::string to_app; std::string action; std::string data; // JSON std::vector required_permissions; }; class MessageBus { public: bool Send(const AppMessage& msg) { // Validate sender if (!IsAppRunning(msg.from_app)) return false; // Check if target accepts this action if (!CanReceive(msg.to_app, msg.action)) return false; // Check permissions for (const auto& perm : msg.required_permissions) { if (!HasPermission(msg.from_app, perm)) return false; } // Deliver (async) QueueMessage(msg); return true; } }; ``` **Manifest declaration**: ```json { "intents": { "receive": [ { "action": "share", "types": ["image/*", "text/plain"] } ], "send": [ "share", "view" ] } } ``` ### Audit Logging **Status**: Planned All security-relevant operations are logged for debugging and forensics. ```cpp enum class AuditEvent { AppStart, AppStop, PermissionCheck, PermissionDenied, NetworkRequest, FileAccess, StorageAccess, TimerCreated, MessageSent, SandboxViolation, ResourceLimitHit }; struct AuditEntry { std::chrono::system_clock::time_point timestamp; std::string app_id; AuditEvent event; std::string detail; bool success; }; class AuditLog { public: void Log(const AuditEntry& entry) { // Thread-safe append std::lock_guard lock(m_mutex); m_entries.push_back(entry); // Trim old entries if (m_entries.size() > MAX_ENTRIES) { m_entries.erase(m_entries.begin(), m_entries.begin() + TRIM_COUNT); } // Log security events to system log if (entry.event == AuditEvent::SandboxViolation || entry.event == AuditEvent::PermissionDenied) { Logger::Warn("[SECURITY] {} - {} - {}", entry.app_id, EventName(entry.event), entry.detail); } } std::vector GetAppLogs(const std::string& app_id, size_t limit); }; ``` ### Coroutine Safety **Status**: Planned Coroutines are allowed but resource-tracked. ```cpp void SetupSafeCoroutines(lua_State* L) { // Allow coroutine.* but track luaL_requiref(L, "coroutine", luaopen_coroutine, 1); lua_pop(L, 1); // Wrap coroutine.create to track lua_getglobal(L, "coroutine"); lua_getfield(L, -1, "create"); lua_pushcclosure(L, [](lua_State* L) -> int { auto* sandbox = GetSandbox(L); // Limit active coroutines if (sandbox->m_coroutine_count >= MAX_COROUTINES) { return luaL_error(L, "too many coroutines"); } // Call original create lua_pushvalue(L, lua_upvalueindex(1)); lua_pushvalue(L, 1); lua_call(L, 1, 1); sandbox->m_coroutine_count++; return 1; }, 1); lua_setfield(L, -2, "create"); lua_pop(L, 1); } ``` ### String Pattern DoS Prevention **Status**: Planned Complex regex patterns can cause catastrophic backtracking. ```cpp int SafeStringMatch(lua_State* L) { size_t slen, plen; const char* s = luaL_checklstring(L, 1, &slen); const char* p = luaL_checklstring(L, 2, &plen); // Limit pattern complexity if (plen > 256) { return luaL_error(L, "pattern too long"); } // Count pattern operators int complexity = 0; for (size_t i = 0; i < plen; i++) { if (p[i] == '*' || p[i] == '+' || p[i] == '-' || p[i] == '?') { complexity++; } } // Reject overly complex patterns if (complexity > 10) { return luaL_error(L, "pattern too complex"); } // Also limit based on string length * complexity if (slen * complexity > 1000000) { return luaL_error(L, "operation too expensive"); } // Call original return original_match(L); } ``` --- ## Runtime Security Audit ### Audit Function ```cpp std::vector AuditSandbox(lua_State* L) { std::vector issues; // Check dangerous globals const char* dangerous[] = { "os", "io", "debug", "package", "ffi", "jit", "dofile", "loadfile", "load", "loadstring", "rawget", "rawset", "rawequal", "rawlen", "collectgarbage", "newproxy", nullptr }; for (const char** p = dangerous; *p; ++p) { lua_getglobal(L, *p); if (!lua_isnil(L, -1)) { issues.push_back({ .severity = Severity::Critical, .type = "dangerous_global", .detail = *p }); } lua_pop(L, 1); } // Check string.dump lua_getglobal(L, "string"); if (lua_istable(L, -1)) { lua_getfield(L, -1, "dump"); if (!lua_isnil(L, -1)) { issues.push_back({ .severity = Severity::Critical, .type = "bytecode_dump", .detail = "string.dump exists" }); } lua_pop(L, 1); } lua_pop(L, 1); // Check instruction hook if (lua_gethook(L) == nullptr) { issues.push_back({ .severity = Severity::High, .type = "no_cpu_limit", .detail = "instruction hook not set" }); } // Check allocator (verify it's our sandbox allocator) // ... return issues; } ``` ### Startup Audit ```cpp bool LuaSandbox::Initialize() { SetupSandbox(); // Audit before running any app code auto issues = AuditSandbox(L); for (const auto& issue : issues) { if (issue.severity == Severity::Critical) { Logger::Error("Sandbox audit failed: {} - {}", issue.type, issue.detail); return false; } } return true; } ``` --- ## Testing Strategy ### Unit Tests ```cpp // test_sandbox_security.cpp TEST(LuaSandbox, DangerousGlobalsRemoved) { LuaSandbox sandbox(TestContext()); EXPECT_FALSE(sandbox.LoadString("return os")); EXPECT_FALSE(sandbox.LoadString("return io")); EXPECT_FALSE(sandbox.LoadString("return debug")); EXPECT_FALSE(sandbox.LoadString("os.execute('ls')")); EXPECT_FALSE(sandbox.LoadString("io.open('/etc/passwd')")); } TEST(LuaSandbox, BytecodeRejected) { LuaSandbox sandbox(TestContext()); // Try to load bytecode EXPECT_FALSE(sandbox.LoadString("\x1bLuaS\x00...")); EXPECT_TRUE(sandbox.GetLastError().find("binary") != std::string::npos); } TEST(LuaSandbox, MemoryLimitEnforced) { SandboxLimits limits; limits.memory_limit_bytes = 1024 * 1024; // 1 MB LuaSandbox sandbox(TestContext(limits)); EXPECT_FALSE(sandbox.LoadString(R"( local t = {} while true do t[#t+1] = string.rep('x', 100000) end )")); } TEST(LuaSandbox, CPULimitEnforced) { SandboxLimits limits; limits.instruction_limit = 10000; LuaSandbox sandbox(TestContext(limits)); EXPECT_FALSE(sandbox.LoadString("while true do end")); EXPECT_TRUE(sandbox.GetLastError().find("instruction") != std::string::npos); } TEST(LuaSandbox, PathTraversalBlocked) { LuaSandbox sandbox(TestContext()); EXPECT_FALSE(sandbox.LoadString("require('../../../etc/passwd')")); EXPECT_FALSE(sandbox.LoadString("require('..\\\\..\\\\..\\\\windows\\\\system32\\\\config')")); EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')")); EXPECT_FALSE(sandbox.LoadString("require('C:\\\\Windows\\\\System32\\\\config')")); } TEST(LuaSandbox, MetatableProtected) { LuaSandbox sandbox(TestContext()); // Cannot access string metatable internals EXPECT_TRUE(sandbox.LoadString(R"( local mt = getmetatable("") assert(mt == "string", "metatable should be protected") )")); // Cannot modify globals EXPECT_FALSE(sandbox.LoadString("_G.print = nil")); } TEST(LuaSandbox, SafeOperationsWork) { LuaSandbox sandbox(TestContext()); EXPECT_TRUE(sandbox.LoadString(R"( -- Math local x = math.sin(1.5) + math.floor(3.7) -- String local s = string.format("hello %d", 42) local upper = string.upper("test") -- Table local t = {1, 2, 3} table.insert(t, 4) table.sort(t) -- Iteration for i, v in ipairs(t) do end for k, v in pairs({a=1, b=2}) do end -- Error handling local ok, err = pcall(function() error("test") end) assert(not ok) -- Type checks assert(type({}) == "table") assert(type("") == "string") )")); } ``` ### Fuzzing ```cpp // Fuzz the sandbox with random Lua code void FuzzSandbox() { std::random_device rd; std::mt19937 gen(rd()); for (int i = 0; i < 10000; i++) { std::string code = GenerateRandomLuaCode(gen); LuaSandbox sandbox(TestContext()); sandbox.LoadString(code); // Should never crash // Verify sandbox integrity after each run auto issues = AuditSandbox(sandbox.GetState()); ASSERT(issues.empty()) << "Sandbox compromised by: " << code; } } ``` --- ## Implementation Status ### Core Sandbox (Milestone 1) | Component | Status | File | |-----------|--------|------| | LuaSandbox class | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.h/cpp` | | Custom allocator | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | | Instruction hook | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | | Globals removal | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | | Bytecode prevention | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | | Safe require | ❌ Planned | Milestone 4 | | Metatable protection | ✅ Implemented | `src/main/cpp/sandbox/lua_sandbox.cpp` | | Path validation | ❌ Planned | Milestone 4 | ### APIs | Component | Status | File | |-----------|--------|------| | Storage API | ❌ Planned | Milestone 7 | | Timer API | ❌ Planned | Milestone 5 | | Network API | ❌ Planned | Milestone 9-10 | | RmlUi bridge | ❌ Planned | Milestone 20 | ### Security | Component | Status | File | |-----------|--------|------| | Permission gate | ❌ Planned | Milestone 2 | | Audit logging | ❌ Planned | Milestone 3 | | Network sandbox | ❌ Planned | Milestone 9 | | Message bus | ❌ Planned | Milestone 18 | ### Testing | Component | Status | File | |-----------|--------|------| | Unit tests | ✅ Implemented | `sandbox-test/` (11 tests) | | Integration tests | ❌ Planned | Milestone 19 | | Fuzzing | ❌ Planned | Milestone 19 | --- ## Additional Security Measures (Continued) ### JSON Parsing Security **Status**: Planned Untrusted JSON from network/storage must be safely parsed. ```cpp struct JsonLimits { size_t max_depth = 32; size_t max_string_length = 1024 * 1024; // 1 MB size_t max_array_elements = 100000; size_t max_object_keys = 10000; }; int SafeJsonDecode(lua_State* L) { size_t len; const char* json = luaL_checklstring(L, 1, &len); // Size check if (len > 10 * 1024 * 1024) { // 10 MB max return luaL_error(L, "JSON too large"); } auto* sandbox = GetSandbox(L); try { auto parsed = ParseJsonWithLimits(json, len, sandbox->json_limits); PushJsonValue(L, parsed); return 1; } catch (const JsonDepthError&) { return luaL_error(L, "JSON nesting too deep"); } catch (const JsonSizeError&) { return luaL_error(L, "JSON element too large"); } } ``` **Lua API**: ```lua local data = json.decode(json_string) -- Safe parsing local str = json.encode(table) -- Safe encoding ``` ### Cryptographic APIs **Status**: Planned Provide safe crypto primitives (apps shouldn't implement their own). ```cpp void SetupCryptoAPI(lua_State* L) { lua_newtable(L); // crypto.randomBytes(n) - cryptographically secure random lua_pushcfunction(L, [](lua_State* L) -> int { int n = luaL_checkinteger(L, 1); if (n < 1 || n > 1024) { return luaL_error(L, "invalid byte count (1-1024)"); } std::vector bytes(n); if (!CSPRNG(bytes.data(), n)) { return luaL_error(L, "random generation failed"); } lua_pushlstring(L, reinterpret_cast(bytes.data()), n); return 1; }); lua_setfield(L, -2, "randomBytes"); // crypto.hash(algorithm, data) - SHA-256, SHA-512 lua_pushcfunction(L, [](lua_State* L) -> int { const char* algo = luaL_checkstring(L, 1); size_t len; const char* data = luaL_checklstring(L, 2, &len); if (strcmp(algo, "sha256") == 0) { auto hash = SHA256(data, len); lua_pushlstring(L, hash.data(), hash.size()); return 1; } else if (strcmp(algo, "sha512") == 0) { auto hash = SHA512(data, len); lua_pushlstring(L, hash.data(), hash.size()); return 1; } return luaL_error(L, "unknown algorithm: %s", algo); }); lua_setfield(L, -2, "hash"); // crypto.hmac(algorithm, key, data) // crypto.encrypt(algorithm, key, iv, plaintext) - AES-256-GCM only // crypto.decrypt(algorithm, key, iv, ciphertext) lua_setglobal(L, "crypto"); } ``` ### Random Number Security **Status**: Planned `math.random()` uses a per-app seed to prevent cross-app prediction. ```cpp void SetupSafeRandom(lua_State* L) { auto* sandbox = GetSandbox(L); // Seed with cryptographic random + app ID hash uint64_t seed; CSPRNG(&seed, sizeof(seed)); seed ^= std::hash{}(sandbox->app_id); // Store RNG state in registry auto* rng = new std::mt19937_64(seed); lua_pushlightuserdata(L, rng); lua_setfield(L, LUA_REGISTRYINDEX, "__rng"); // Replace math.random lua_getglobal(L, "math"); lua_pushcfunction(L, [](lua_State* L) -> int { lua_getfield(L, LUA_REGISTRYINDEX, "__rng"); auto* rng = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); int nargs = lua_gettop(L); if (nargs == 0) { // [0, 1) std::uniform_real_distribution dist(0.0, 1.0); lua_pushnumber(L, dist(*rng)); } else if (nargs == 1) { // [1, n] int n = luaL_checkinteger(L, 1); std::uniform_int_distribution dist(1, n); lua_pushinteger(L, dist(*rng)); } else { // [m, n] int m = luaL_checkinteger(L, 1); int n = luaL_checkinteger(L, 2); std::uniform_int_distribution dist(m, n); lua_pushinteger(L, dist(*rng)); } return 1; }); lua_setfield(L, -2, "random"); // Remove math.randomseed (prevent manipulation) lua_pushnil(L); lua_setfield(L, -2, "randomseed"); lua_pop(L, 1); } ``` ### Environment Fingerprinting Prevention **Status**: Planned Limit information leakage about device/user. ```cpp void SetupEnvironmentAPI(lua_State* L) { lua_newtable(L); // Only expose safe, non-identifying info lua_pushstring(L, "1.0.0"); lua_setfield(L, -2, "mosis_version"); lua_pushinteger(L, 540); lua_setfield(L, -2, "screen_width"); lua_pushinteger(L, 960); lua_setfield(L, -2, "screen_height"); // NOT exposed: // - Device model // - OS version // - Unique identifiers // - Installed apps list // - Precise time (only second precision) // - Memory/CPU info // - Network interfaces lua_setglobal(L, "env"); } // Time with reduced precision int SafeTime(lua_State* L) { // Round to seconds (no milliseconds) time_t now = time(nullptr); lua_pushinteger(L, static_cast(now)); return 1; } ``` ### Rate Limiting **Status**: Planned Prevent abuse of expensive operations. ```cpp class RateLimiter { public: struct Limit { int max_calls; std::chrono::milliseconds window; }; bool Check(const std::string& operation) { auto now = std::chrono::steady_clock::now(); auto& bucket = m_buckets[operation]; // Remove old entries while (!bucket.empty() && now - bucket.front() > m_limits[operation].window) { bucket.pop_front(); } // Check limit if (bucket.size() >= m_limits[operation].max_calls) { return false; } bucket.push_back(now); return true; } private: std::unordered_map m_limits = { {"network.request", {100, std::chrono::minutes(1)}}, {"storage.write", {1000, std::chrono::minutes(1)}}, {"crypto.hash", {1000, std::chrono::seconds(1)}}, {"timer.create", {100, std::chrono::seconds(1)}}, }; std::unordered_map> m_buckets; }; // Usage int NetworkRequest(lua_State* L) { auto* sandbox = GetSandbox(L); if (!sandbox->rate_limiter.Check("network.request")) { return luaL_error(L, "rate limit exceeded"); } // ... proceed with request } ``` ### Resource Cleanup on Termination **Status**: Planned Ensure all resources are released when app stops. ```cpp class LuaSandbox { public: ~LuaSandbox() { Cleanup(); } void Cleanup() { // Cancel all timers m_timer_manager->CancelAll(m_context.app_id); // Close all network connections m_network_manager->CloseAll(m_context.app_id); // Release all file handles m_file_manager->CloseAll(m_context.app_id); // Clear message queue m_message_bus->ClearPending(m_context.app_id); // Release Lua callbacks for (int ref : m_callback_refs) { luaL_unref(L, LUA_REGISTRYINDEX, ref); } // Close Lua state (releases memory) if (L) { lua_close(L); L = nullptr; } // Log cleanup AuditLog::Log({ .app_id = m_context.app_id, .event = AuditEvent::AppStop, .detail = "cleanup completed" }); } private: std::vector m_callback_refs; }; ``` ### App Signature Verification **Status**: Planned Verify app packages haven't been tampered with. ```cpp struct AppSignature { std::string algorithm; // "ed25519" std::string public_key; // Base64 std::string signature; // Base64 }; class PackageVerifier { public: bool Verify(const std::string& mpkg_path) { // Read manifest auto manifest = ReadManifest(mpkg_path); if (!manifest) return false; // Check signature exists if (!manifest->signature) { // Unsigned packages allowed in dev mode only return IsDevMode(); } // Get developer's public key from store/registry auto pub_key = GetDeveloperKey(manifest->author_id); if (!pub_key) return false; // Compute package hash (all files except signature) auto hash = ComputePackageHash(mpkg_path); // Verify signature return VerifyEd25519( pub_key->data(), hash.data(), hash.size(), Base64Decode(manifest->signature->signature) ); } private: std::vector ComputePackageHash(const std::string& path) { // Hash all files in deterministic order SHA256_CTX ctx; SHA256_Init(&ctx); for (const auto& file : ListFilesRecursive(path)) { if (file.name == "signature.json") continue; // Include filename in hash SHA256_Update(&ctx, file.name.data(), file.name.size()); // Include content auto content = ReadFile(file.path); SHA256_Update(&ctx, content.data(), content.size()); } std::vector hash(32); SHA256_Final(hash.data(), &ctx); return hash; } }; ``` ### Debug Mode Restrictions **Status**: Planned Development features disabled in production. ```cpp class DebugMode { public: static bool IsEnabled() { #ifdef NDEBUG return false; #else return s_debug_enabled; #endif } // Only available in debug mode static void EnableHotReload(LuaSandbox* sandbox) { if (!IsEnabled()) return; // ... } static void EnableInspector(LuaSandbox* sandbox) { if (!IsEnabled()) return; // ... } // Log debug state for security audit static void LogDebugState() { if (IsEnabled()) { Logger::Warn("[SECURITY] Debug mode enabled - " "sandbox restrictions relaxed"); } } private: static bool s_debug_enabled; }; // In production builds, these are completely compiled out #ifdef NDEBUG #define DEBUG_ONLY(x) ((void)0) #else #define DEBUG_ONLY(x) x #endif ``` ### Clipboard Isolation **Status**: Planned Apps can only access clipboard with permission and user gesture. ```cpp class ClipboardManager { public: // Requires permission + recent user gesture std::optional Read(LuaSandbox* sandbox) { if (!sandbox->HasPermission("clipboard.read")) { return std::nullopt; } // Must be within 1 second of user gesture (click/tap) if (!sandbox->HasRecentUserGesture(1000)) { Logger::Warn("Clipboard read blocked - no recent user gesture"); return std::nullopt; } return GetSystemClipboard(); } bool Write(LuaSandbox* sandbox, const std::string& text) { if (!sandbox->HasPermission("clipboard.write")) { return false; } // Size limit if (text.size() > 1024 * 1024) { return false; } SetSystemClipboard(text); return true; } }; ``` ### Stack Depth Limiting **Status**: Planned Prevent stack overflow via deep recursion. ```cpp void SetupStackLimit(lua_State* L, int max_depth) { // Set C stack limit // Note: Lua 5.4 has LUAI_MAXCSTACK // Also use debug hook to check Lua call depth lua_sethook(L, [](lua_State* L, lua_Debug* ar) { lua_Debug info; int depth = 0; while (lua_getstack(L, depth, &info)) { depth++; } auto* sandbox = GetSandbox(L); if (depth > sandbox->m_limits.stack_depth) { luaL_error(L, "stack overflow (depth %d)", depth); } }, LUA_MASKCALL, 0); } ``` --- ## Security Testing Checklist ### Before Release - [ ] All dangerous globals removed - [ ] Bytecode loading blocked - [ ] Memory limits enforced - [ ] CPU limits enforced - [ ] Path traversal blocked - [ ] Metatable protection working - [ ] Permission checks on all APIs - [ ] Network domain filtering working - [ ] Rate limiting active - [ ] Audit logging enabled - [ ] Debug mode disabled in release - [ ] Fuzzing completed with no crashes - [ ] Security audit by external party ### Per-App Review (Store Submission) - [ ] Manifest permissions justified - [ ] No obfuscated code - [ ] Network domains declared - [ ] Package signature valid - [ ] No known vulnerability patterns - [ ] Resource usage reasonable --- ## Virtual Hardware Interfaces Mosis provides virtualized hardware interfaces that apps can access through permission-gated APIs. Each interface has specific security requirements based on the sensitivity of the data it exposes. ### Hardware Permission Model ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Permission Categories │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ NORMAL (auto-granted) DANGEROUS (user prompt required) │ │ ├── storage.app ├── camera │ │ ├── network.metadata ├── microphone │ │ ├── system.vibrate ├── location.fine │ │ └── audio.playback ├── location.coarse │ │ ├── contacts.read │ │ SIGNATURE (system apps only) ├── contacts.write │ │ ├── system.settings ├── storage.shared │ │ ├── app.install ├── network.internet │ │ └── hardware.control ├── bluetooth │ │ ├── phone.call │ │ ├── sms.send │ │ └── sensors.body │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Hardware Security Threat Model | Interface | Threat | Impact | Mitigation | |-----------|--------|--------|------------| | **Camera** | Silent recording | Critical privacy | Indicator, gesture gate | | **Microphone** | Eavesdropping | Critical privacy | Indicator, gesture gate | | **Location** | Tracking | High privacy | Precision reduction, rate limit | | **Network** | Data exfiltration | High privacy | Domain filtering, inspection | | **Filesystem** | Data theft/corruption | High integrity | Path sandboxing | | **Database** | SQL injection | High integrity | Prepared statements only | | **Bluetooth** | Device tracking | Medium privacy | Pairing consent | | **Sensors** | Fingerprinting | Medium privacy | Reduced precision | | **Speaker** | Audio spam | Low annoyance | Volume limits, rate limit | --- ## Network Interface Security ### Network Sandbox Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ App Lua Code │ │ │ │ │ network.request(url, opts) │ │ │ │ ├──────────────────────────────▼──────────────────────────────────────┤ │ NetworkSandbox │ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │ │ │ Permission │ │ Domain │ │ Content │ │ │ │ Check │→ │ Validation │→ │ Inspection │ │ │ └─────────────┘ └──────────────┘ └────────────────────────────┘ │ │ │ │ │ ┌─────────▼──────────┐ │ │ │ Rate Limiter │ │ │ └─────────┬──────────┘ │ │ │ │ ├──────────────────────────────▼──────────────────────────────────────┤ │ System HTTP Client │ │ (libcurl / Android HttpClient) │ └─────────────────────────────────────────────────────────────────────┘ ``` ### HTTP Request Security **Status**: Designed ```cpp struct HttpRequestPolicy { // Domain restrictions std::vector allowed_domains; // Whitelist from manifest std::vector blocked_domains; // System blacklist bool allow_localhost = false; // Block 127.0.0.1, ::1 bool allow_private_ips = false; // Block 10.x, 192.168.x, etc. bool allow_metadata_ips = false; // Block 169.254.x (cloud metadata) // Protocol restrictions bool require_https = true; // Block plain HTTP std::vector allowed_schemes = {"https"}; // Size limits size_t max_request_body = 10 * 1024 * 1024; // 10 MB size_t max_response_body = 50 * 1024 * 1024; // 50 MB size_t max_header_size = 64 * 1024; // 64 KB // Timeout int connect_timeout_ms = 10000; int read_timeout_ms = 30000; int total_timeout_ms = 60000; // Rate limits int max_concurrent_requests = 6; int max_requests_per_minute = 100; }; class HttpRequestValidator { public: enum class Result { Allowed, PermissionDenied, DomainBlocked, PrivateIPBlocked, MetadataIPBlocked, LocalhostBlocked, InsecureProtocol, RequestTooLarge, RateLimitExceeded }; Result Validate(const HttpRequest& req, const AppContext& ctx) { // 1. Permission check if (!ctx.HasPermission("network.internet")) { return Result::PermissionDenied; } // 2. Parse URL auto url = ParseUrl(req.url); if (!url) return Result::DomainBlocked; // 3. Protocol check if (m_policy.require_https && url->scheme != "https") { return Result::InsecureProtocol; } // 4. Resolve hostname to IP (detect IP-based bypass) auto ips = ResolveHostname(url->host); // 5. Block private IPs for (const auto& ip : ips) { if (IsPrivateIP(ip) && !m_policy.allow_private_ips) { return Result::PrivateIPBlocked; } if (IsMetadataIP(ip) && !m_policy.allow_metadata_ips) { return Result::MetadataIPBlocked; } if (IsLocalhost(ip) && !m_policy.allow_localhost) { return Result::LocalhostBlocked; } } // 6. Domain whitelist/blacklist if (!m_policy.allowed_domains.empty()) { if (!MatchesDomainList(url->host, m_policy.allowed_domains)) { return Result::DomainBlocked; } } if (MatchesDomainList(url->host, m_policy.blocked_domains)) { return Result::DomainBlocked; } // 7. Size check if (req.body.size() > m_policy.max_request_body) { return Result::RequestTooLarge; } // 8. Rate limit if (!m_rate_limiter.Check(ctx.app_id, "http_request")) { return Result::RateLimitExceeded; } return Result::Allowed; } private: bool IsPrivateIP(const std::string& ip) { // IPv4 private ranges if (ip.starts_with("10.")) return true; if (ip.starts_with("172.")) { int second_octet = std::stoi(ip.substr(4, ip.find('.', 4) - 4)); if (second_octet >= 16 && second_octet <= 31) return true; } if (ip.starts_with("192.168.")) return true; // Link-local if (ip.starts_with("169.254.")) return true; // IPv6 private ranges if (ip.starts_with("fc") || ip.starts_with("fd")) return true; // ULA if (ip.starts_with("fe80:")) return true; // Link-local return false; } bool IsMetadataIP(const std::string& ip) { // AWS/GCP/Azure metadata endpoints return ip == "169.254.169.254" || ip == "metadata.google.internal" || ip.starts_with("fd00:ec2::"); } }; ``` ### WebSocket Security **Status**: Designed ```cpp struct WebSocketPolicy { bool enabled = true; int max_connections_per_app = 5; size_t max_message_size = 1 * 1024 * 1024; // 1 MB int ping_interval_ms = 30000; int idle_timeout_ms = 300000; // 5 minutes // Same domain validation as HTTP std::vector allowed_origins; }; class WebSocketManager { public: struct Connection { uint32_t id; std::string app_id; std::string url; ConnectionState state; std::chrono::steady_clock::time_point created_at; std::chrono::steady_clock::time_point last_activity; size_t bytes_sent = 0; size_t bytes_received = 0; }; std::optional Connect(LuaSandbox* sandbox, const std::string& url) { // Permission check if (!sandbox->HasPermission("network.websocket")) { return std::nullopt; } // Validate URL (same as HTTP) auto result = m_validator.Validate({.url = url}, sandbox->context()); if (result != HttpRequestValidator::Result::Allowed) { return std::nullopt; } // Connection limit if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) { return std::nullopt; } // Create connection auto conn = CreateConnection(url); conn.app_id = sandbox->app_id(); m_connections[conn.id] = conn; return conn.id; } bool Send(LuaSandbox* sandbox, uint32_t conn_id, const std::string& data) { auto it = m_connections.find(conn_id); if (it == m_connections.end()) return false; // Verify ownership if (it->second.app_id != sandbox->app_id()) return false; // Size limit if (data.size() > m_policy.max_message_size) return false; // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "ws_send")) return false; // Send it->second.bytes_sent += data.size(); it->second.last_activity = std::chrono::steady_clock::now(); return DoSend(conn_id, data); } void CloseAll(const std::string& app_id) { // Called when app stops for (auto it = m_connections.begin(); it != m_connections.end(); ) { if (it->second.app_id == app_id) { DoClose(it->first); it = m_connections.erase(it); } else { ++it; } } } private: std::unordered_map m_connections; WebSocketPolicy m_policy; HttpRequestValidator m_validator; RateLimiter m_rate_limiter; }; ``` ### Network Lua API ```lua -- HTTP Requests (requires "network.internet" permission) local response = network.request({ url = "https://api.example.com/data", method = "POST", -- GET, POST, PUT, DELETE, PATCH headers = { ["Content-Type"] = "application/json", ["Authorization"] = "Bearer " .. token }, body = json.encode({key = "value"}), timeout = 30000 -- milliseconds }) -- Response structure -- response.status: number (200, 404, etc.) -- response.headers: table -- response.body: string -- response.error: string (nil if success) -- WebSocket (requires "network.websocket" permission) local ws = network.websocket("wss://api.example.com/ws") ws:on("open", function() ws:send(json.encode({type = "hello"})) end) ws:on("message", function(data) local msg = json.decode(data) print("Received:", msg.type) end) ws:on("close", function(code, reason) print("Closed:", code, reason) end) ws:on("error", function(err) print("Error:", err) end) ws:close() ``` ### Manifest Declaration ```json { "permissions": ["network.internet", "network.websocket"], "network": { "allowed_domains": [ "api.example.com", "*.cdn.example.com", "wss://realtime.example.com" ], "allow_http": false, "max_connections": 10 } } ``` --- ## Filesystem Interface Security ### Virtual Filesystem Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Physical Storage │ │ /data/data/com.omixlab.mosis/ │ │ ├── apps/ │ │ │ ├── com.example.app1/ │ │ │ │ ├── data/ ← App-private storage │ │ │ │ ├── cache/ ← Clearable cache │ │ │ │ └── temp/ ← Session-only │ │ │ └── com.example.app2/ │ │ ├── shared/ │ │ │ ├── photos/ ← Shared media (permission required) │ │ │ ├── downloads/ │ │ │ └── documents/ │ │ └── system/ ← System apps only │ └─────────────────────────────────────────────────────────────────────┘ │ ┌───────▼───────┐ │ VirtualFS API │ └───────┬───────┘ │ ┌───────────────────────────────────▼─────────────────────────────────┐ │ App's Virtual View │ │ / │ │ ├── data/ → /apps//data/ (auto-granted) │ │ ├── cache/ → /apps//cache/ (auto-granted) │ │ ├── temp/ → /apps//temp/ (auto-granted) │ │ ├── shared/ → /shared/ (permission required) │ │ └── (nothing else visible) │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Filesystem Security Implementation **Status**: Designed ```cpp struct FilesystemPolicy { // Storage quotas size_t max_app_storage = 100 * 1024 * 1024; // 100 MB per app size_t max_cache_storage = 50 * 1024 * 1024; // 50 MB cache size_t max_file_size = 50 * 1024 * 1024; // 50 MB single file // File limits size_t max_files_per_directory = 10000; size_t max_path_depth = 32; size_t max_filename_length = 255; // Allowed extensions (whitelist approach) std::vector allowed_extensions = { ".txt", ".json", ".xml", ".html", ".css", ".js", ".lua", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp3", ".ogg", ".wav", ".mp4", ".webm", ".db", ".sqlite" }; // Blocked patterns std::vector blocked_patterns = { "*.exe", "*.dll", "*.so", "*.dylib", // Executables "*.sh", "*.bat", "*.cmd", "*.ps1", // Scripts ".*", // Hidden files }; }; class VirtualFilesystem { public: VirtualFilesystem(const std::string& app_id, const std::string& base_path) : m_app_id(app_id) , m_base_path(base_path) , m_app_root(base_path + "/apps/" + app_id) {} // Resolve virtual path to physical path with security checks std::optional ResolvePath(const std::string& virtual_path, AccessMode mode) { // 1. Validate path format if (!IsValidPathFormat(virtual_path)) { AuditLog::Log({m_app_id, AuditEvent::FileAccess, "invalid path format: " + virtual_path, false}); return std::nullopt; } // 2. Parse virtual path auto parts = SplitPath(virtual_path); if (parts.empty()) return std::nullopt; // 3. Determine physical root based on virtual root std::string physical_root; bool requires_permission = false; if (parts[0] == "data") { physical_root = m_app_root + "/data"; } else if (parts[0] == "cache") { physical_root = m_app_root + "/cache"; } else if (parts[0] == "temp") { physical_root = m_app_root + "/temp"; } else if (parts[0] == "shared") { physical_root = m_base_path + "/shared"; requires_permission = true; } else { // Unknown root - access denied return std::nullopt; } // 4. Permission check for shared storage if (requires_permission) { std::string perm = (mode == AccessMode::Read) ? "storage.shared.read" : "storage.shared.write"; if (!HasPermission(perm)) { AuditLog::Log({m_app_id, AuditEvent::PermissionDenied, perm, false}); return std::nullopt; } } // 5. Build full path std::filesystem::path full_path = physical_root; for (size_t i = 1; i < parts.size(); ++i) { full_path /= parts[i]; } // 6. Canonicalize and verify containment std::filesystem::path canonical; try { if (mode == AccessMode::Read) { // Must exist for read canonical = std::filesystem::canonical(full_path); } else { // May not exist for write - canonicalize parent canonical = std::filesystem::weakly_canonical(full_path); } } catch (const std::filesystem::filesystem_error&) { return std::nullopt; } // 7. Verify path is within allowed root (prevent traversal) std::string canonical_str = canonical.string(); if (!canonical_str.starts_with(physical_root)) { AuditLog::Log({m_app_id, AuditEvent::SandboxViolation, "path traversal attempt: " + virtual_path, false}); return std::nullopt; } // 8. Check file extension if (!IsAllowedExtension(canonical.extension().string())) { return std::nullopt; } return canonical_str; } // Quota enforcement bool HasStorageQuota(size_t bytes_needed) { size_t current = CalculateDirectorySize(m_app_root + "/data"); return (current + bytes_needed) <= m_policy.max_app_storage; } std::optional GetUsedStorage() { return CalculateDirectorySize(m_app_root + "/data"); } std::optional GetAvailableStorage() { auto used = GetUsedStorage(); if (!used) return std::nullopt; return m_policy.max_app_storage - *used; } private: bool IsValidPathFormat(const std::string& path) { // Must start with / if (path.empty() || path[0] != '/') return false; // No .. components if (path.find("..") != std::string::npos) return false; // No double slashes if (path.find("//") != std::string::npos) return false; // No null bytes if (path.find('\0') != std::string::npos) return false; // Length check if (path.length() > 4096) return false; // Check each component for (const auto& part : SplitPath(path)) { if (part.length() > m_policy.max_filename_length) return false; if (part.empty()) return false; if (MatchesBlockedPattern(part)) return false; } return true; } bool IsAllowedExtension(const std::string& ext) { if (ext.empty()) return true; // Directories std::string lower_ext = ToLower(ext); return std::find(m_policy.allowed_extensions.begin(), m_policy.allowed_extensions.end(), lower_ext) != m_policy.allowed_extensions.end(); } std::string m_app_id; std::string m_base_path; std::string m_app_root; FilesystemPolicy m_policy; }; ``` ### File Operations with Security Gates ```cpp class SecureFileAPI { public: // Read file std::optional ReadFile(LuaSandbox* sandbox, const std::string& path) { auto physical = m_vfs.ResolvePath(path, AccessMode::Read); if (!physical) return std::nullopt; // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "file_read")) { return std::nullopt; } // Read with size limit std::ifstream file(*physical, std::ios::binary); if (!file) return std::nullopt; file.seekg(0, std::ios::end); size_t size = file.tellg(); if (size > m_policy.max_file_size) return std::nullopt; file.seekg(0, std::ios::beg); std::string content(size, '\0'); file.read(content.data(), size); AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, "read: " + path + " (" + std::to_string(size) + " bytes)", true}); return content; } // Write file bool WriteFile(LuaSandbox* sandbox, const std::string& path, const std::string& content) { auto physical = m_vfs.ResolvePath(path, AccessMode::Write); if (!physical) return false; // Size check if (content.size() > m_policy.max_file_size) return false; // Quota check if (!m_vfs.HasStorageQuota(content.size())) { AuditLog::Log({sandbox->app_id(), AuditEvent::ResourceLimitHit, "storage quota exceeded", false}); return false; } // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "file_write")) { return false; } // Create parent directories std::filesystem::create_directories( std::filesystem::path(*physical).parent_path()); // Write atomically (temp file + rename) std::string temp_path = *physical + ".tmp." + GenerateRandomId(); { std::ofstream file(temp_path, std::ios::binary); if (!file) return false; file.write(content.data(), content.size()); } std::error_code ec; std::filesystem::rename(temp_path, *physical, ec); if (ec) { std::filesystem::remove(temp_path); return false; } AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, "write: " + path + " (" + std::to_string(content.size()) + " bytes)", true}); return true; } // List directory std::optional> ListDirectory(LuaSandbox* sandbox, const std::string& path) { auto physical = m_vfs.ResolvePath(path, AccessMode::Read); if (!physical) return std::nullopt; std::vector entries; for (const auto& entry : std::filesystem::directory_iterator(*physical)) { if (entries.size() >= m_policy.max_files_per_directory) break; FileInfo info; info.name = entry.path().filename().string(); info.is_directory = entry.is_directory(); info.size = entry.is_regular_file() ? entry.file_size() : 0; info.modified = GetModificationTime(entry.path()); entries.push_back(info); } return entries; } // Delete file bool Delete(LuaSandbox* sandbox, const std::string& path) { auto physical = m_vfs.ResolvePath(path, AccessMode::Write); if (!physical) return false; // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "file_delete")) { return false; } std::error_code ec; bool removed = std::filesystem::remove(*physical, ec); AuditLog::Log({sandbox->app_id(), AuditEvent::FileAccess, "delete: " + path, removed}); return removed; } private: VirtualFilesystem m_vfs; FilesystemPolicy m_policy; RateLimiter m_rate_limiter; }; ``` ### Filesystem Lua API ```lua -- File operations (app-private storage is auto-granted) local content = storage.read("/data/config.json") local success = storage.write("/data/config.json", json.encode(config)) -- Directory operations local files = storage.list("/data/saves/") for _, file in ipairs(files) do print(file.name, file.size, file.is_directory) end -- File info local info = storage.info("/data/save.json") -- info.exists, info.size, info.modified, info.is_directory -- Delete storage.delete("/data/old_file.json") -- Create directory storage.mkdir("/data/saves/slot1/") -- Check storage quota local quota = storage.quota() -- quota.used, quota.available, quota.total -- Shared storage (requires "storage.shared.read" / "storage.shared.write") local photos = storage.list("/shared/photos/") storage.write("/shared/documents/report.txt", "Hello") ``` --- ## Database Interface Security ### SQLite Sandbox **Status**: Designed Apps can use SQLite databases with security restrictions. ```cpp struct DatabasePolicy { size_t max_database_size = 50 * 1024 * 1024; // 50 MB size_t max_databases_per_app = 5; size_t max_query_time_ms = 5000; size_t max_result_rows = 10000; bool allow_attach = false; // Prevent ATTACH to other DBs bool allow_load_extension = false; // No extensions }; class SecureDatabase { public: SecureDatabase(const std::string& app_id, const std::string& db_name) : m_app_id(app_id) , m_db_name(db_name) { // Validate database name if (!IsValidDatabaseName(db_name)) { throw std::invalid_argument("invalid database name"); } // Open database in app's data directory std::string path = GetAppDataPath(app_id) + "/databases/" + db_name + ".db"; int rc = sqlite3_open_v2(path.c_str(), &m_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, // Single-threaded access per app nullptr); if (rc != SQLITE_OK) { throw std::runtime_error("failed to open database"); } // Security configuration ConfigureSecurity(); } ~SecureDatabase() { if (m_db) sqlite3_close(m_db); } // Execute query with prepared statement (prevents SQL injection) QueryResult Execute(const std::string& sql, const std::vector& params) { // 1. Validate SQL (basic sanity checks) if (!ValidateSql(sql)) { return {.error = "invalid SQL"}; } // 2. Prepare statement sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { return {.error = sqlite3_errmsg(m_db)}; } // 3. Bind parameters for (size_t i = 0; i < params.size(); ++i) { BindParameter(stmt, i + 1, params[i]); } // 4. Execute with timeout QueryResult result; auto start = std::chrono::steady_clock::now(); while (true) { rc = sqlite3_step(stmt); // Check timeout auto elapsed = std::chrono::steady_clock::now() - start; if (elapsed > std::chrono::milliseconds(m_policy.max_query_time_ms)) { sqlite3_finalize(stmt); return {.error = "query timeout"}; } if (rc == SQLITE_ROW) { // Check row limit if (result.rows.size() >= m_policy.max_result_rows) { result.truncated = true; break; } result.rows.push_back(ExtractRow(stmt)); } else if (rc == SQLITE_DONE) { break; } else { result.error = sqlite3_errmsg(m_db); break; } } result.changes = sqlite3_changes(m_db); result.last_insert_id = sqlite3_last_insert_rowid(m_db); sqlite3_finalize(stmt); return result; } private: void ConfigureSecurity() { // Disable dangerous features sqlite3_db_config(m_db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, nullptr); // Set authorizer to block dangerous operations sqlite3_set_authorizer(m_db, Authorizer, this); // Set database size limit sqlite3_soft_heap_limit64(m_policy.max_database_size); // Set busy timeout sqlite3_busy_timeout(m_db, 1000); } static int Authorizer(void* user_data, int action, const char* arg1, const char* arg2, const char* arg3, const char* arg4) { auto* self = static_cast(user_data); switch (action) { case SQLITE_ATTACH: case SQLITE_DETACH: // Block ATTACH/DETACH to prevent accessing other databases return SQLITE_DENY; case SQLITE_PRAGMA: // Allow only safe pragmas if (arg1 && !IsSafePragma(arg1)) { return SQLITE_DENY; } break; case SQLITE_FUNCTION: // Block dangerous functions if (arg2 && IsDangerousFunction(arg2)) { return SQLITE_DENY; } break; } return SQLITE_OK; } static bool IsSafePragma(const char* pragma) { // Whitelist of safe pragmas static const std::unordered_set safe = { "table_info", "index_list", "foreign_keys", "journal_mode", "synchronous", "cache_size" }; return safe.count(pragma) > 0; } static bool IsDangerousFunction(const char* func) { // Block functions that can access filesystem or execute code static const std::unordered_set dangerous = { "load_extension", "readfile", "writefile", "edit", "fts3_tokenizer", "sqlite_compileoption_get" }; return dangerous.count(func) > 0; } bool ValidateSql(const std::string& sql) { std::string upper = ToUpper(sql); // Block dangerous statements if (upper.find("ATTACH") != std::string::npos) return false; if (upper.find("DETACH") != std::string::npos) return false; if (upper.find("LOAD_EXTENSION") != std::string::npos) return false; return true; } sqlite3* m_db = nullptr; std::string m_app_id; std::string m_db_name; DatabasePolicy m_policy; }; ``` ### Database Lua API ```lua -- Open database (stored in /data/databases/) local db = database.open("myapp") -- Create table db:execute([[ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at INTEGER DEFAULT (strftime('%s', 'now')) ) ]]) -- Insert with parameters (SQL injection safe) db:execute("INSERT INTO users (name, email) VALUES (?, ?)", {"John", "john@example.com"}) -- Query with parameters local result = db:query("SELECT * FROM users WHERE name LIKE ?", {"%John%"}) for _, row in ipairs(result.rows) do print(row.id, row.name, row.email) end -- Transaction db:transaction(function() db:execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", {100, 1}) db:execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", {100, 2}) end) -- Close db:close() ``` --- ## Camera Interface Security ### Camera Security Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ App Lua Code │ │ camera.capture(callback) │ │ │ │ ├──────────────────────────────▼──────────────────────────────────────┤ │ CameraManager │ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────────┐ │ │ │ Permission │ │ Active │ │ Frame Rate │ │ │ │ Check │→ │ Indicator │→ │ Limiter │ │ │ └─────────────┘ └──────────────┘ └────────────────────────────┘ │ │ │ │ ├──────────────────────────────▼──────────────────────────────────────┤ │ Game Engine Camera │ │ (Unity RenderTexture / Unreal SceneCapture) │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Camera Security Implementation **Status**: Designed ```cpp struct CameraPolicy { // Access control bool require_user_gesture = true; // Must be triggered by tap/click int max_active_sessions = 1; // Only one app can use camera // Frame limits int max_fps = 30; int max_resolution_width = 1920; int max_resolution_height = 1080; // Privacy bool show_indicator = true; // Always show recording indicator int indicator_min_duration_ms = 500; // Indicator shows at least 500ms bool allow_silent_capture = false; // Require shutter sound // Storage bool auto_save_to_gallery = false; // App must explicitly save std::string watermark_text = ""; // Optional watermark }; class CameraManager { public: struct CameraSession { uint32_t id; std::string app_id; CameraFacing facing; Resolution resolution; bool active; std::chrono::steady_clock::time_point started_at; }; // Start camera session std::optional StartSession(LuaSandbox* sandbox, const CameraOptions& opts) { // 1. Permission check if (!sandbox->HasPermission("camera")) { AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, "camera", false}); return std::nullopt; } // 2. User gesture check if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) { AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, "blocked - no user gesture", false}); return std::nullopt; } // 3. Check exclusive access if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) { return std::nullopt; // Another app is using camera } // 4. Validate resolution Resolution res = opts.resolution; res.width = std::min(res.width, m_policy.max_resolution_width); res.height = std::min(res.height, m_policy.max_resolution_height); // 5. Start session CameraSession session{ .id = ++m_next_session_id, .app_id = sandbox->app_id(), .facing = opts.facing, .resolution = res, .active = true, .started_at = std::chrono::steady_clock::now() }; m_active_session = session; // 6. Show indicator if (m_policy.show_indicator) { ShowCameraIndicator(true); } // 7. Request frames from game engine m_hardware_camera->Start(res, [this, session_id = session.id](Frame frame) { OnFrameReceived(session_id, frame); }); AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, "session started", true}); return session.id; } // Capture single frame std::optional CaptureFrame(LuaSandbox* sandbox, uint32_t session_id) { // Verify session ownership if (!m_active_session || m_active_session->id != session_id || m_active_session->app_id != sandbox->app_id()) { return std::nullopt; } // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "camera_capture")) { return std::nullopt; } // Get latest frame auto frame = m_latest_frame; if (!frame) return std::nullopt; // Play shutter sound (if configured) if (!m_policy.allow_silent_capture) { PlayShutterSound(); } // Apply watermark if configured if (!m_policy.watermark_text.empty()) { ApplyWatermark(frame, m_policy.watermark_text); } return frame; } // Stop session void StopSession(LuaSandbox* sandbox, uint32_t session_id) { if (!m_active_session || m_active_session->id != session_id || m_active_session->app_id != sandbox->app_id()) { return; } m_hardware_camera->Stop(); // Keep indicator visible for minimum duration auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) { std::this_thread::sleep_for( std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed); } ShowCameraIndicator(false); m_active_session.reset(); AuditLog::Log({sandbox->app_id(), AuditEvent::CameraAccess, "session stopped", true}); } // Stop all sessions for app (called on app termination) void StopAllSessions(const std::string& app_id) { if (m_active_session && m_active_session->app_id == app_id) { m_hardware_camera->Stop(); ShowCameraIndicator(false); m_active_session.reset(); } } private: void OnFrameReceived(uint32_t session_id, Frame frame) { if (!m_active_session || m_active_session->id != session_id) { return; } // Apply frame rate limit auto now = std::chrono::steady_clock::now(); auto elapsed = now - m_last_frame_time; if (elapsed < std::chrono::milliseconds(1000 / m_policy.max_fps)) { return; } m_last_frame_time = now; m_latest_frame = frame; // Notify app (if callback registered) if (m_frame_callback) { m_frame_callback(frame); } } std::optional m_active_session; std::optional m_latest_frame; std::chrono::steady_clock::time_point m_last_frame_time; CameraPolicy m_policy; IHardwareCamera* m_hardware_camera; RateLimiter m_rate_limiter; uint32_t m_next_session_id = 0; }; ``` ### Camera Lua API ```lua -- Request camera permission (shows system dialog) local granted = permissions.request("camera") if not granted then return end -- Start camera session local session = camera.start({ facing = "back", -- "back" or "front" resolution = {width = 1280, height = 720} }) -- Preview frames session:onFrame(function(frame) -- frame.width, frame.height, frame.data (base64 JPEG) ui.setImage("preview", frame.data) end) -- Capture photo local photo = session:capture() if photo then -- Save to app storage storage.write("/data/photos/photo_" .. os.time() .. ".jpg", base64.decode(photo.data)) -- Or save to shared gallery (requires storage.shared.write) storage.write("/shared/photos/MyApp/photo.jpg", base64.decode(photo.data)) end -- Stop session session:stop() ``` --- ## Microphone Interface Security ### Microphone Security Implementation **Status**: Designed ```cpp struct MicrophonePolicy { // Access control bool require_user_gesture = true; int max_active_sessions = 1; // Audio limits int max_sample_rate = 48000; int max_duration_ms = 300000; // 5 minutes max recording int max_buffer_size = 4096; // Privacy bool show_indicator = true; int indicator_min_duration_ms = 500; bool allow_background_recording = false; }; class MicrophoneManager { public: struct AudioSession { uint32_t id; std::string app_id; int sample_rate; int channels; bool active; std::chrono::steady_clock::time_point started_at; size_t samples_recorded = 0; }; std::optional StartRecording(LuaSandbox* sandbox, const AudioOptions& opts) { // 1. Permission check if (!sandbox->HasPermission("microphone")) { AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, "microphone", false}); return std::nullopt; } // 2. User gesture check if (m_policy.require_user_gesture && !sandbox->HasRecentUserGesture(1000)) { return std::nullopt; } // 3. Exclusive access if (HasActiveSession() && m_active_session->app_id != sandbox->app_id()) { return std::nullopt; } // 4. Background recording check if (!m_policy.allow_background_recording && !sandbox->IsAppForeground()) { return std::nullopt; } // 5. Validate options int sample_rate = std::min(opts.sample_rate, m_policy.max_sample_rate); AudioSession session{ .id = ++m_next_session_id, .app_id = sandbox->app_id(), .sample_rate = sample_rate, .channels = opts.channels, .active = true, .started_at = std::chrono::steady_clock::now() }; m_active_session = session; // 6. Show indicator if (m_policy.show_indicator) { ShowMicrophoneIndicator(true); } // 7. Start hardware recording m_hardware_mic->Start(sample_rate, opts.channels, [this, session_id = session.id](AudioBuffer buffer) { OnAudioReceived(session_id, buffer); }); AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess, "recording started", true}); return session.id; } void StopRecording(LuaSandbox* sandbox, uint32_t session_id) { if (!m_active_session || m_active_session->id != session_id || m_active_session->app_id != sandbox->app_id()) { return; } m_hardware_mic->Stop(); // Minimum indicator duration auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; if (elapsed < std::chrono::milliseconds(m_policy.indicator_min_duration_ms)) { std::this_thread::sleep_for( std::chrono::milliseconds(m_policy.indicator_min_duration_ms) - elapsed); } ShowMicrophoneIndicator(false); m_active_session.reset(); AuditLog::Log({sandbox->app_id(), AuditEvent::MicrophoneAccess, "recording stopped", true}); } private: void OnAudioReceived(uint32_t session_id, AudioBuffer buffer) { if (!m_active_session || m_active_session->id != session_id) { return; } // Check duration limit auto elapsed = std::chrono::steady_clock::now() - m_active_session->started_at; if (elapsed > std::chrono::milliseconds(m_policy.max_duration_ms)) { // Auto-stop after max duration StopRecordingInternal(session_id); return; } m_active_session->samples_recorded += buffer.samples; // Deliver to app callback if (m_audio_callback) { m_audio_callback(buffer); } } std::optional m_active_session; MicrophonePolicy m_policy; IHardwareMicrophone* m_hardware_mic; uint32_t m_next_session_id = 0; }; ``` ### Microphone Lua API ```lua -- Request permission local granted = permissions.request("microphone") if not granted then return end -- Start recording local session = microphone.start({ sample_rate = 44100, channels = 1 }) -- Receive audio data session:onAudio(function(buffer) -- buffer.samples: number of samples -- buffer.data: base64 encoded PCM data end) -- Stop recording session:stop() -- Get recorded audio local audio = session:getRecording() storage.write("/data/recordings/voice.wav", audio) ``` --- ## Speaker/Audio Output Security ### Audio Output Policy **Status**: Designed ```cpp struct AudioOutputPolicy { float max_volume = 1.0f; int max_concurrent_sounds = 8; int max_sound_duration_ms = 300000; // 5 minutes size_t max_audio_data_size = 50 * 1024 * 1024; // 50 MB bool require_audio_focus = true; // Respect other apps }; class AudioOutputManager { public: std::optional PlaySound(LuaSandbox* sandbox, const AudioData& audio, const PlaybackOptions& opts) { // No dangerous permission needed for audio output, // but still track and limit // 1. Validate audio data if (audio.size() > m_policy.max_audio_data_size) { return std::nullopt; } // 2. Check concurrent sounds limit int app_sounds = CountAppSounds(sandbox->app_id()); if (app_sounds >= m_policy.max_concurrent_sounds) { return std::nullopt; } // 3. Clamp volume float volume = std::clamp(opts.volume, 0.0f, m_policy.max_volume); // 4. Request audio focus if (m_policy.require_audio_focus) { if (!RequestAudioFocus(sandbox->app_id())) { return std::nullopt; } } // 5. Create sound instance Sound sound{ .id = ++m_next_id, .app_id = sandbox->app_id(), .volume = volume, .looping = opts.loop, .started_at = std::chrono::steady_clock::now() }; m_sounds[sound.id] = sound; m_hardware_audio->Play(audio, volume, opts.loop); return sound.id; } void StopSound(LuaSandbox* sandbox, uint32_t sound_id) { auto it = m_sounds.find(sound_id); if (it == m_sounds.end() || it->second.app_id != sandbox->app_id()) { return; } m_hardware_audio->Stop(sound_id); m_sounds.erase(it); } void StopAllSounds(const std::string& app_id) { for (auto it = m_sounds.begin(); it != m_sounds.end(); ) { if (it->second.app_id == app_id) { m_hardware_audio->Stop(it->first); it = m_sounds.erase(it); } else { ++it; } } } private: std::unordered_map m_sounds; AudioOutputPolicy m_policy; IHardwareAudio* m_hardware_audio; uint32_t m_next_id = 0; }; ``` ### Audio Lua API ```lua -- Play sound (no permission required) local sound = audio.play("/data/sounds/notification.wav", { volume = 0.8, loop = false }) -- Control playback sound:pause() sound:resume() sound:stop() -- System sounds audio.playSystem("notification") audio.playSystem("click") -- Vibration (requires nothing or system.vibrate on some platforms) audio.vibrate(100) -- 100ms audio.vibrate({100, 50, 100}) -- pattern: vibrate, pause, vibrate ``` --- ## Location Interface Security ### Location Security Implementation **Status**: Designed ```cpp struct LocationPolicy { // Precision control int coarse_accuracy_meters = 1000; // City-level int fine_accuracy_meters = 10; // GPS precision // Rate limiting int min_update_interval_ms = 1000; // Max 1 update/sec int background_update_interval_ms = 60000; // 1/min in background // Privacy bool require_user_gesture_for_first = true; bool allow_background_updates = false; int max_cached_locations = 100; }; class LocationManager { public: std::optional GetLocation(LuaSandbox* sandbox, LocationAccuracy accuracy) { // 1. Permission check std::string perm = (accuracy == LocationAccuracy::Fine) ? "location.fine" : "location.coarse"; if (!sandbox->HasPermission(perm)) { AuditLog::Log({sandbox->app_id(), AuditEvent::PermissionDenied, perm, false}); return std::nullopt; } // 2. Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "location")) { return std::nullopt; } // 3. Get location from hardware auto location = m_hardware_location->GetCurrent(); if (!location) return std::nullopt; // 4. Apply precision reduction for coarse if (accuracy == LocationAccuracy::Coarse) { location = ReducePrecision(*location, m_policy.coarse_accuracy_meters); } AuditLog::Log({sandbox->app_id(), AuditEvent::LocationAccess, "single location request", true}); return location; } std::optional WatchLocation(LuaSandbox* sandbox, LocationAccuracy accuracy, LocationCallback callback) { std::string perm = (accuracy == LocationAccuracy::Fine) ? "location.fine" : "location.coarse"; if (!sandbox->HasPermission(perm)) { return std::nullopt; } // Background check if (!sandbox->IsAppForeground() && !m_policy.allow_background_updates) { return std::nullopt; } uint32_t watch_id = ++m_next_watch_id; LocationWatch watch{ .id = watch_id, .app_id = sandbox->app_id(), .accuracy = accuracy, .callback = callback, .last_update = std::chrono::steady_clock::time_point::min() }; m_watches[watch_id] = watch; // Start hardware updates if first watch if (m_watches.size() == 1) { m_hardware_location->StartUpdates([this](Location loc) { OnLocationUpdate(loc); }); } return watch_id; } void ClearWatch(LuaSandbox* sandbox, uint32_t watch_id) { auto it = m_watches.find(watch_id); if (it == m_watches.end() || it->second.app_id != sandbox->app_id()) { return; } m_watches.erase(it); if (m_watches.empty()) { m_hardware_location->StopUpdates(); } } private: Location ReducePrecision(const Location& loc, int meters) { // Round coordinates to reduce precision // 0.01 degree ≈ 1.1km at equator double precision = meters / 111000.0; // degrees per meter Location reduced = loc; reduced.latitude = std::round(loc.latitude / precision) * precision; reduced.longitude = std::round(loc.longitude / precision) * precision; reduced.accuracy = std::max(loc.accuracy, static_cast(meters)); return reduced; } void OnLocationUpdate(Location loc) { auto now = std::chrono::steady_clock::now(); for (auto& [id, watch] : m_watches) { // Check update interval int interval = m_policy.min_update_interval_ms; auto elapsed = now - watch.last_update; if (elapsed < std::chrono::milliseconds(interval)) { continue; } // Apply precision reduction Location delivered = loc; if (watch.accuracy == LocationAccuracy::Coarse) { delivered = ReducePrecision(loc, m_policy.coarse_accuracy_meters); } watch.last_update = now; watch.callback(delivered); } } std::unordered_map m_watches; LocationPolicy m_policy; IHardwareLocation* m_hardware_location; RateLimiter m_rate_limiter; uint32_t m_next_watch_id = 0; }; ``` ### Location Lua API ```lua -- Request permission local granted = permissions.request("location.fine") -- or "location.coarse" -- Get current location local loc = location.get() if loc then print(loc.latitude, loc.longitude, loc.accuracy) end -- Watch location changes local watch_id = location.watch(function(loc) print("Moved to:", loc.latitude, loc.longitude) end, { accuracy = "fine", -- "fine" or "coarse" interval = 5000 -- minimum ms between updates }) -- Stop watching location.clearWatch(watch_id) ``` --- ## Sensor Interface Security ### Sensor Security (Accelerometer, Gyroscope, etc.) **Status**: Designed ```cpp struct SensorPolicy { // Rate limiting (prevent fingerprinting via sensor noise patterns) int max_sample_rate_hz = 60; // Precision reduction float accelerometer_precision = 0.01f; // m/s² float gyroscope_precision = 0.001f; // rad/s // Background access bool allow_background_sensors = false; // Body sensors (heart rate, etc.) require special permission std::vector body_sensors = {"heart_rate", "blood_pressure"}; }; class SensorManager { public: std::optional RegisterListener(LuaSandbox* sandbox, SensorType type, SensorCallback callback) { // Body sensors require dangerous permission if (IsBodySensor(type) && !sandbox->HasPermission("sensors.body")) { return std::nullopt; } // Background check if (!sandbox->IsAppForeground() && !m_policy.allow_background_sensors) { return std::nullopt; } uint32_t listener_id = ++m_next_listener_id; SensorListener listener{ .id = listener_id, .app_id = sandbox->app_id(), .type = type, .callback = callback, .last_reading = std::chrono::steady_clock::time_point::min() }; m_listeners[listener_id] = listener; // Start sensor if first listener EnableSensorIfNeeded(type); return listener_id; } void UnregisterListener(LuaSandbox* sandbox, uint32_t listener_id) { auto it = m_listeners.find(listener_id); if (it == m_listeners.end() || it->second.app_id != sandbox->app_id()) { return; } SensorType type = it->second.type; m_listeners.erase(it); // Stop sensor if no more listeners DisableSensorIfUnneeded(type); } private: void OnSensorReading(SensorType type, SensorReading reading) { auto now = std::chrono::steady_clock::now(); int min_interval_ms = 1000 / m_policy.max_sample_rate_hz; for (auto& [id, listener] : m_listeners) { if (listener.type != type) continue; // Rate limit auto elapsed = now - listener.last_reading; if (elapsed < std::chrono::milliseconds(min_interval_ms)) { continue; } // Apply precision reduction SensorReading reduced = ReducePrecision(reading, type); listener.last_reading = now; listener.callback(reduced); } } SensorReading ReducePrecision(const SensorReading& reading, SensorType type) { SensorReading reduced = reading; float precision = 0.01f; switch (type) { case SensorType::Accelerometer: precision = m_policy.accelerometer_precision; break; case SensorType::Gyroscope: precision = m_policy.gyroscope_precision; break; default: break; } reduced.x = std::round(reading.x / precision) * precision; reduced.y = std::round(reading.y / precision) * precision; reduced.z = std::round(reading.z / precision) * precision; return reduced; } std::unordered_map m_listeners; SensorPolicy m_policy; uint32_t m_next_listener_id = 0; }; ``` ### Sensor Lua API ```lua -- Motion sensors (no special permission) local accel_id = sensors.listen("accelerometer", function(reading) print("Acceleration:", reading.x, reading.y, reading.z) end) local gyro_id = sensors.listen("gyroscope", function(reading) print("Rotation:", reading.x, reading.y, reading.z) end) -- Stop listening sensors.unlisten(accel_id) sensors.unlisten(gyro_id) -- Body sensors (requires "sensors.body" permission) local granted = permissions.request("sensors.body") if granted then sensors.listen("heart_rate", function(reading) print("Heart rate:", reading.value, "bpm") end) end ``` --- ## Bluetooth Interface Security ### Bluetooth Security Implementation **Status**: Designed ```cpp struct BluetoothPolicy { // Discovery bool allow_discovery = true; int discovery_timeout_seconds = 30; int max_discovered_devices = 50; // Connections int max_connections_per_app = 3; bool require_pairing_consent = true; // Data size_t max_transfer_size = 1 * 1024 * 1024; // 1 MB }; class BluetoothManager { public: // Discover nearby devices std::optional StartDiscovery(LuaSandbox* sandbox, DiscoveryCallback callback) { if (!sandbox->HasPermission("bluetooth")) { return std::nullopt; } // Only one discovery at a time per app if (HasActiveDiscovery(sandbox->app_id())) { return std::nullopt; } uint32_t discovery_id = ++m_next_discovery_id; Discovery discovery{ .id = discovery_id, .app_id = sandbox->app_id(), .callback = callback, .devices = {}, .started_at = std::chrono::steady_clock::now() }; m_discoveries[discovery_id] = discovery; // Start hardware discovery with timeout m_hardware_bt->StartDiscovery( m_policy.discovery_timeout_seconds, [this, discovery_id](BluetoothDevice device) { OnDeviceDiscovered(discovery_id, device); }); return discovery_id; } // Connect to device std::optional Connect(LuaSandbox* sandbox, const std::string& device_address) { if (!sandbox->HasPermission("bluetooth")) { return std::nullopt; } // Connection limit if (CountAppConnections(sandbox->app_id()) >= m_policy.max_connections_per_app) { return std::nullopt; } // Require user consent for pairing if (m_policy.require_pairing_consent) { if (!ShowPairingDialog(device_address)) { return std::nullopt; } } // Connect auto conn = m_hardware_bt->Connect(device_address); if (!conn) return std::nullopt; uint32_t conn_id = ++m_next_conn_id; BluetoothConnection connection{ .id = conn_id, .app_id = sandbox->app_id(), .device_address = device_address, .hardware_handle = *conn }; m_connections[conn_id] = connection; return conn_id; } private: void OnDeviceDiscovered(uint32_t discovery_id, BluetoothDevice device) { auto it = m_discoveries.find(discovery_id); if (it == m_discoveries.end()) return; // Limit discovered devices if (it->second.devices.size() >= m_policy.max_discovered_devices) { return; } it->second.devices.push_back(device); it->second.callback(device); } std::unordered_map m_discoveries; std::unordered_map m_connections; BluetoothPolicy m_policy; IHardwareBluetooth* m_hardware_bt; uint32_t m_next_discovery_id = 0; uint32_t m_next_conn_id = 0; }; ``` ### Bluetooth Lua API ```lua -- Request permission local granted = permissions.request("bluetooth") if not granted then return end -- Discover devices local discovery = bluetooth.discover(function(device) print("Found:", device.name, device.address) end) -- Stop discovery discovery:stop() -- Connect to device (shows pairing dialog) local conn = bluetooth.connect("AA:BB:CC:DD:EE:FF") if conn then -- Send data conn:send("Hello") -- Receive data conn:onData(function(data) print("Received:", data) end) -- Disconnect conn:close() end ``` --- ## Contacts Interface Security ### Contacts Security Implementation **Status**: Designed ```cpp struct ContactsPolicy { // Access scope bool allow_read_all = true; bool allow_write = true; bool allow_delete = true; // Rate limits int max_contacts_per_query = 1000; int max_writes_per_minute = 100; // Privacy bool require_purpose_string = true; // App must explain why bool log_all_access = true; }; class ContactsManager { public: std::optional> GetContacts(LuaSandbox* sandbox, const ContactQuery& query) { if (!sandbox->HasPermission("contacts.read")) { return std::nullopt; } // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_read")) { return std::nullopt; } // Query contacts auto contacts = m_contacts_store->Query(query); // Limit results if (contacts.size() > m_policy.max_contacts_per_query) { contacts.resize(m_policy.max_contacts_per_query); } AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, "read " + std::to_string(contacts.size()) + " contacts", true}); return contacts; } bool CreateContact(LuaSandbox* sandbox, const Contact& contact) { if (!sandbox->HasPermission("contacts.write")) { return false; } // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) { return false; } // Validate contact data if (!ValidateContact(contact)) { return false; } bool success = m_contacts_store->Create(contact); AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, "create contact: " + contact.name, success}); return success; } bool DeleteContact(LuaSandbox* sandbox, const std::string& contact_id) { if (!sandbox->HasPermission("contacts.write")) { return false; } // Rate limit if (!m_rate_limiter.Check(sandbox->app_id(), "contacts_write")) { return false; } bool success = m_contacts_store->Delete(contact_id); AuditLog::Log({sandbox->app_id(), AuditEvent::ContactsAccess, "delete contact: " + contact_id, success}); return success; } private: bool ValidateContact(const Contact& contact) { // Name required if (contact.name.empty()) return false; if (contact.name.length() > 256) return false; // Validate phone numbers for (const auto& phone : contact.phones) { if (phone.number.length() > 50) return false; } // Validate emails for (const auto& email : contact.emails) { if (!IsValidEmail(email.address)) return false; } return true; } ContactsPolicy m_policy; IContactsStore* m_contacts_store; RateLimiter m_rate_limiter; }; ``` ### Contacts Lua API ```lua -- Request permission local granted = permissions.request("contacts.read") -- Query contacts local contacts = contacts.getAll() for _, c in ipairs(contacts) do print(c.name, c.phones[1], c.emails[1]) end -- Search contacts local results = contacts.search("John") -- Get single contact local contact = contacts.get(contact_id) -- Create contact (requires contacts.write) contacts.create({ name = "John Doe", phones = {{type = "mobile", number = "+1234567890"}}, emails = {{type = "home", address = "john@example.com"}} }) -- Update contact contacts.update(contact_id, { name = "John Smith" }) -- Delete contact contacts.delete(contact_id) ``` --- ## Implementation Status - Virtual Hardware | Interface | Design | Implementation | Testing | |-----------|--------|----------------|---------| | Network HTTP | ✅ Complete | ❌ Not started | ❌ | | Network WebSocket | ✅ Complete | ❌ Not started | ❌ | | Filesystem | ✅ Complete | ❌ Not started | ❌ | | Database | ✅ Complete | ❌ Not started | ❌ | | Camera | ✅ Complete | ❌ Not started | ❌ | | Microphone | ✅ Complete | ❌ Not started | ❌ | | Speaker/Audio | ✅ Complete | ❌ Not started | ❌ | | Location | ✅ Complete | ❌ Not started | ❌ | | Sensors | ✅ Complete | ❌ Not started | ❌ | | Bluetooth | ✅ Complete | ❌ Not started | ❌ | | Contacts | ✅ Complete | ❌ Not started | ❌ | --- ## References - [Lua 5.4 Manual - Sandboxing](https://www.lua.org/manual/5.4/manual.html#4.6) - [Sandboxing Lua](http://lua-users.org/wiki/SandBoxes) - [Lapis Lua Sandbox](https://github.com/leafo/lapis/blob/master/lapis/util/sandbox.moon) - [Cloudflare Workers Lua Isolation](https://blog.cloudflare.com/cloudflare-workers-unleashed/) - [OWASP Sandboxing Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Sandboxing_Cheat_Sheet.html) - [Android Permission Model](https://developer.android.com/guide/topics/permissions/overview) - [iOS App Sandbox Design Guide](https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/) - [SQLite Security](https://www.sqlite.org/security.html) --- *Last updated: 2026-01-18*