Files
MosisService/SANDBOX_MILESTONE_15.md

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.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

// 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 permission
  • Test_SensorAutoGrantsEnvironmental - Proximity/light auto-granted
  • Test_SensorReducesPrecision - Values rounded to 2 decimals
  • Test_SensorLimitsFrequency - Frequency capped at 60 Hz
  • Test_SensorSubscriptionLimit - Max 10 subscriptions enforced
  • Test_SensorCleansUpOnShutdown - Cleanup on shutdown
  • 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