13 KiB
13 KiB
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.motionpermission - 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
- SensorInterface class - Sensor subscription management
- SensorSubscription class - Individual sensor stream
- Lua sensors API -
sensors.subscribe(), subscription methods - 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
// sensor_interface.h
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
#include <unordered_map>
#include <chrono>
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<void(const SensorData&)>;
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<bool> 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<SensorSubscription> Subscribe(
SensorType type,
const SensorOptions& options,
std::string& error
);
// Subscribe by sensor name
std::shared_ptr<SensorSubscription> 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<SensorSubscription> 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<int, std::shared_ptr<SensorSubscription>> 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:
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
-- 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
-- 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
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
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
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
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
bool Test_SensorSubscriptionLimit(std::string& error_msg) {
mosis::SensorInterface sensors("test.app");
sensors.SetMotionPermission(true);
std::vector<std::shared_ptr<mosis::SensorSubscription>> 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
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
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:
Test_SensorRequiresPermission- Motion sensors require permissionTest_SensorAutoGrantsEnvironmental- Proximity/light auto-grantedTest_SensorReducesPrecision- Values rounded to 2 decimalsTest_SensorLimitsFrequency- Frequency capped at 60 HzTest_SensorSubscriptionLimit- Max 10 subscriptions enforcedTest_SensorCleansUpOnShutdown- Cleanup on shutdownTest_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:
- Use SensorManager via JNI
- Register sensor event listeners
- Apply precision reduction before callbacks
- Enforce frequency via sensor delay settings
Security Considerations
- Permission required: Motion sensors need
sensors.motionpermission - Precision reduction: Values rounded to 2 decimals to prevent fingerprinting
- Frequency limit: Maximum 60 Hz prevents high-frequency abuse
- Subscription limit: Max 10 subscriptions per app
- Cleanup: All subscriptions stopped when app stops
Privacy Features
- Reduced precision: Prevents unique device identification via sensor calibration
- No raw sensor access: Apps only get processed, reduced-precision data
- Audit logging: All sensor subscriptions logged for security review
Next Steps
After Milestone 15 passes:
- Milestone 16: Virtual Hardware - Bluetooth