3570 lines
106 KiB
Markdown
3570 lines
106 KiB
Markdown
# 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<LuaSandbox*>(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<std::string> allowed_domains; // Whitelist
|
|
std::vector<std::string> 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<std::string> 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<AuditEntry> 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<SecurityIssue> AuditSandbox(lua_State* L) {
|
|
std::vector<SecurityIssue> 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<uint8_t> bytes(n);
|
|
if (!CSPRNG(bytes.data(), n)) {
|
|
return luaL_error(L, "random generation failed");
|
|
}
|
|
|
|
lua_pushlstring(L, reinterpret_cast<char*>(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<std::string>{}(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<std::mt19937_64*>(lua_touserdata(L, -1));
|
|
lua_pop(L, 1);
|
|
|
|
int nargs = lua_gettop(L);
|
|
if (nargs == 0) {
|
|
// [0, 1)
|
|
std::uniform_real_distribution<double> 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<int> 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<int> 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<lua_Integer>(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<std::string, Limit> 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<std::string, std::deque<TimePoint>> 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<int> 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<uint8_t> 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<uint8_t> 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<std::string> 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<std::string> allowed_domains; // Whitelist from manifest
|
|
std::vector<std::string> 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<std::string> 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<std::string> 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<uint32_t> 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<uint32_t, Connection> 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/<app_id>/data/ (auto-granted) │
|
|
│ ├── cache/ → /apps/<app_id>/cache/ (auto-granted) │
|
|
│ ├── temp/ → /apps/<app_id>/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<std::string> 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<std::string> 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<std::string> 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<size_t> GetUsedStorage() {
|
|
return CalculateDirectorySize(m_app_root + "/data");
|
|
}
|
|
|
|
std::optional<size_t> 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<std::string> 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<std::vector<FileInfo>> ListDirectory(LuaSandbox* sandbox,
|
|
const std::string& path) {
|
|
auto physical = m_vfs.ResolvePath(path, AccessMode::Read);
|
|
if (!physical) return std::nullopt;
|
|
|
|
std::vector<FileInfo> 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<SqlValue>& 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<SecureDatabase*>(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<std::string> 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<std::string> 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<uint32_t> 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<ImageData> 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<CameraSession> m_active_session;
|
|
std::optional<Frame> 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<uint32_t> 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<AudioSession> 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<uint32_t> 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<uint32_t, Sound> 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<Location> 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<uint32_t> 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<float>(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<uint32_t, LocationWatch> 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<std::string> body_sensors = {"heart_rate", "blood_pressure"};
|
|
};
|
|
|
|
class SensorManager {
|
|
public:
|
|
std::optional<uint32_t> 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<uint32_t, SensorListener> 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<uint32_t> 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<uint32_t> 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<uint32_t, Discovery> m_discoveries;
|
|
std::unordered_map<uint32_t, BluetoothConnection> 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<std::vector<Contact>> 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*
|