move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
501
docs/SANDBOX_MILESTONE_15.md
Normal file
501
docs/SANDBOX_MILESTONE_15.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# 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 <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:
|
||||
|
||||
```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<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
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user