# Milestone 15: Virtual Hardware - Sensors **Status**: Complete **Goal**: Motion sensors with fingerprinting prevention. --- ## Overview This milestone implements secure sensor access for Lua apps: - Motion sensors (accelerometer, gyroscope, magnetometer) require `sensors.motion` permission - Environmental sensors (proximity, ambient light) are auto-granted - Precision reduction to prevent device fingerprinting - Frequency limiting to 60 Hz maximum - Subscription-based API for continuous updates - Automatic cleanup on app stop ### Key Deliverables 1. **SensorInterface class** - Sensor subscription management 2. **SensorSubscription class** - Individual sensor stream 3. **Lua sensors API** - `sensors.subscribe()`, subscription methods 4. **Fingerprinting prevention** - Precision reduction and frequency limits --- ## File Structure ``` src/main/cpp/sandbox/ ├── sensor_interface.h # NEW - Sensor API header └── sensor_interface.cpp # NEW - Sensor implementation ``` --- ## Implementation Details ### 1. SensorInterface Class ```cpp // sensor_interface.h #pragma once #include #include #include #include #include #include #include #include struct lua_State; namespace mosis { enum class SensorType { Accelerometer, // Requires sensors.motion Gyroscope, // Requires sensors.motion Magnetometer, // Requires sensors.motion Proximity, // Auto-granted AmbientLight // Auto-granted }; struct SensorData { double x = 0.0; // Primary axis / value double y = 0.0; // Secondary axis (if applicable) double z = 0.0; // Tertiary axis (if applicable) int64_t timestamp = 0; // Milliseconds since epoch }; struct SensorOptions { int frequency = 60; // Hz, capped at 60 }; class SensorSubscription { public: using DataCallback = std::function; SensorSubscription(int id, SensorType type, const SensorOptions& options); ~SensorSubscription(); int GetId() const { return m_id; } SensorType GetType() const { return m_type; } bool IsActive() const { return m_active; } int GetFrequency() const { return m_frequency; } void Start(); void Stop(); void SetOnData(DataCallback cb) { m_on_data = std::move(cb); } // For mock mode - simulate sensor data void SimulateData(const SensorData& data); private: int m_id; SensorType m_type; int m_frequency; std::atomic m_active{false}; DataCallback m_on_data; mutable std::mutex m_mutex; }; class SensorInterface { public: SensorInterface(const std::string& app_id); ~SensorInterface(); // Check if app has motion sensor permission bool HasMotionPermission() const { return m_has_motion_permission; } void SetMotionPermission(bool granted) { m_has_motion_permission = granted; } // Subscribe to a sensor // Returns subscription on success, nullptr on failure std::shared_ptr Subscribe( SensorType type, const SensorOptions& options, std::string& error ); // Subscribe by sensor name std::shared_ptr Subscribe( const std::string& sensor_name, const SensorOptions& options, std::string& error ); // Stop a specific subscription void Unsubscribe(int subscription_id); // Stop all subscriptions for this app void UnsubscribeAll(); // Get active subscription count size_t GetActiveSubscriptionCount() const; // Cleanup on app stop void Shutdown(); // For testing void SetMockMode(bool enabled) { m_mock_mode = enabled; } bool IsMockMode() const { return m_mock_mode; } // Get subscription by ID std::shared_ptr GetSubscription(int id); // Check if sensor type requires permission static bool RequiresPermission(SensorType type); // Parse sensor name to type static bool ParseSensorType(const std::string& name, SensorType& out_type); private: std::string m_app_id; std::unordered_map> m_subscriptions; mutable std::mutex m_mutex; bool m_mock_mode = true; bool m_has_motion_permission = false; int m_next_subscription_id = 1; static constexpr int MAX_SUBSCRIPTIONS = 10; static constexpr int MAX_FREQUENCY = 60; // Hz // Apply precision reduction to prevent fingerprinting SensorData ApplyPrecision(const SensorData& data) const; void CleanupStoppedSubscriptions(); }; // Register sensors.* APIs as globals void RegisterSensorAPI(lua_State* L, SensorInterface* sensors); } // namespace mosis ``` ### 2. Sensor Types and Permissions | Sensor | Permission Required | Data Fields | |--------|---------------------|-------------| | Accelerometer | `sensors.motion` | x, y, z (m/s²) | | Gyroscope | `sensors.motion` | x, y, z (rad/s) | | Magnetometer | `sensors.motion` | x, y, z (µT) | | Proximity | None (auto-granted) | x (distance in cm, or 0/1 for near/far) | | Ambient Light | None (auto-granted) | x (lux) | ### 3. Fingerprinting Prevention Values are rounded to 2 decimal places to prevent device fingerprinting: ```cpp SensorData SensorInterface::ApplyPrecision(const SensorData& data) const { SensorData result = data; result.x = std::round(data.x * 100.0) / 100.0; result.y = std::round(data.y * 100.0) / 100.0; result.z = std::round(data.z * 100.0) / 100.0; return result; } ``` ### 4. Frequency Limiting Maximum update frequency is capped at 60 Hz: - Prevents high-frequency sensor abuse - Reduces battery impact - Sufficient for most app use cases ### 5. Lua API ```lua -- Subscribe to accelerometer (requires sensors.motion permission) local sub = sensors.subscribe("accelerometer", function(data) print("Accel:", data.x, data.y, data.z) print("Time:", data.timestamp) end, { frequency = 60 -- Hz, max 60 }) -- Check if subscription is active if sub:isActive() then print("Subscription ID:", sub:getId()) end -- Stop subscription sub:stop() -- Subscribe to proximity (auto-granted, no permission needed) local proxSub = sensors.subscribe("proximity", function(data) if data.x < 5 then print("Object is near") end end) -- Get active subscription count local count = sensors.getSubscriptionCount() -- Unsubscribe all sensors.unsubscribeAll() ``` ### 6. Sensor Names ```lua -- Valid sensor names "accelerometer" -- Accelerometer (requires permission) "gyroscope" -- Gyroscope (requires permission) "magnetometer" -- Magnetometer (requires permission) "proximity" -- Proximity sensor (auto-granted) "light" -- Ambient light sensor (auto-granted) ``` --- ## Test Cases ### Test 1: Requires Permission for Motion Sensors ```cpp bool Test_SensorRequiresPermission(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); // No permissions granted std::string err; auto sub = sensors.Subscribe("accelerometer", {}, err); EXPECT_TRUE(sub == nullptr); EXPECT_TRUE(err.find("permission") != std::string::npos); return true; } ``` ### Test 2: Auto-Grants Environmental Sensors ```cpp bool Test_SensorAutoGrantsEnvironmental(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); // No motion permission std::string err; // Proximity should work without permission auto proxSub = sensors.Subscribe("proximity", {}, err); EXPECT_TRUE(proxSub != nullptr); EXPECT_TRUE(proxSub->IsActive()); // Light should work without permission auto lightSub = sensors.Subscribe("light", {}, err); EXPECT_TRUE(lightSub != nullptr); EXPECT_TRUE(lightSub->IsActive()); return true; } ``` ### Test 3: Reduces Precision ```cpp bool Test_SensorReducesPrecision(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); sensors.SetMotionPermission(true); std::string err; auto sub = sensors.Subscribe("accelerometer", {}, err); EXPECT_TRUE(sub != nullptr); mosis::SensorData received; bool got_data = false; sub->SetOnData([&](const mosis::SensorData& data) { got_data = true; received = data; }); // Simulate high-precision data mosis::SensorData precise; precise.x = 9.80665123456; precise.y = 0.12345678901; precise.z = -0.98765432109; sub->SimulateData(precise); EXPECT_TRUE(got_data); // Values should be rounded to 2 decimal places EXPECT_TRUE(std::abs(received.x - 9.81) < 0.001); EXPECT_TRUE(std::abs(received.y - 0.12) < 0.001); EXPECT_TRUE(std::abs(received.z - (-0.99)) < 0.001); return true; } ``` ### Test 4: Limits Frequency ```cpp bool Test_SensorLimitsFrequency(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); sensors.SetMotionPermission(true); mosis::SensorOptions options; options.frequency = 120; // Request 120 Hz std::string err; auto sub = sensors.Subscribe("gyroscope", options, err); EXPECT_TRUE(sub != nullptr); // Should be capped at 60 Hz EXPECT_TRUE(sub->GetFrequency() <= 60); return true; } ``` ### Test 5: Subscription Limit ```cpp bool Test_SensorSubscriptionLimit(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); sensors.SetMotionPermission(true); std::vector> subs; std::string err; // Create MAX_SUBSCRIPTIONS (10) for (int i = 0; i < 10; i++) { auto sub = sensors.Subscribe("accelerometer", {}, err); EXPECT_TRUE(sub != nullptr); subs.push_back(sub); } // 11th should fail auto extra = sensors.Subscribe("accelerometer", {}, err); EXPECT_TRUE(extra == nullptr); EXPECT_TRUE(err.find("limit") != std::string::npos || err.find("maximum") != std::string::npos); return true; } ``` ### Test 6: Cleanup on Shutdown ```cpp bool Test_SensorCleansUpOnShutdown(std::string& error_msg) { mosis::SensorInterface sensors("test.app"); sensors.SetMotionPermission(true); std::string err; auto sub1 = sensors.Subscribe("accelerometer", {}, err); auto sub2 = sensors.Subscribe("gyroscope", {}, err); EXPECT_TRUE(sensors.GetActiveSubscriptionCount() == 2); sensors.Shutdown(); EXPECT_TRUE(sensors.GetActiveSubscriptionCount() == 0); EXPECT_TRUE(!sub1->IsActive()); EXPECT_TRUE(!sub2->IsActive()); return true; } ``` ### Test 7: Lua Integration ```cpp bool Test_SensorLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::SensorInterface sensors("test.app"); mosis::RegisterSensorAPI(sandbox.GetState(), &sensors); std::string script = R"lua( -- Test that sensors global exists if not sensors then error("sensors global not found") end if not sensors.subscribe then error("sensors.subscribe not found") end if not sensors.unsubscribeAll then error("sensors.unsubscribeAll not found") end if not sensors.getSubscriptionCount then error("sensors.getSubscriptionCount not found") end -- Subscription count should be 0 initially if sensors.getSubscriptionCount() ~= 0 then error("should have no active subscriptions initially") end )lua"; bool ok = sandbox.LoadString(script, "sensor_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } ``` --- ## Acceptance Criteria All tests must pass: - [x] `Test_SensorRequiresPermission` - Motion sensors require permission - [x] `Test_SensorAutoGrantsEnvironmental` - Proximity/light auto-granted - [x] `Test_SensorReducesPrecision` - Values rounded to 2 decimals - [x] `Test_SensorLimitsFrequency` - Frequency capped at 60 Hz - [x] `Test_SensorSubscriptionLimit` - Max 10 subscriptions enforced - [x] `Test_SensorCleansUpOnShutdown` - Cleanup on shutdown - [x] `Test_SensorLuaIntegration` - Lua API works --- ## Dependencies - Milestone 1 (LuaSandbox) - Milestone 2 (PermissionGate) --- ## Notes ### Desktop vs Android Implementation For desktop testing, SensorInterface operates in mock mode: - Subscriptions track state but don't receive real sensor data - Data can be simulated via `SimulateData()` for testing - Precision reduction and frequency limits are enforced normally On Android, the real implementation would: 1. Use SensorManager via JNI 2. Register sensor event listeners 3. Apply precision reduction before callbacks 4. Enforce frequency via sensor delay settings ### Security Considerations 1. **Permission required**: Motion sensors need `sensors.motion` permission 2. **Precision reduction**: Values rounded to 2 decimals to prevent fingerprinting 3. **Frequency limit**: Maximum 60 Hz prevents high-frequency abuse 4. **Subscription limit**: Max 10 subscriptions per app 5. **Cleanup**: All subscriptions stopped when app stops ### Privacy Features 1. **Reduced precision**: Prevents unique device identification via sensor calibration 2. **No raw sensor access**: Apps only get processed, reduced-precision data 3. **Audit logging**: All sensor subscriptions logged for security review --- ## Next Steps After Milestone 15 passes: 1. Milestone 16: Virtual Hardware - Bluetooth