Flipper Zero/Rogue AP Detector/baseline.c
From charlesreid1
Main article: Flipper Zero/Rogue AP Detector
Requirements
- Define the data structure for a baselined AP and the public functions for managing the baseline database (baseline_ap_t struct) that is slightly different from the scanner's ap_info_t struct)
- Include fields for tracking the observed signal strength range and the last time the AP was seen (for aging out old data and detecting signal anomalies later)
- Handle storage limitations on Flipper Zero by using SD card for storage:
- File Format: Use a smple binary file (short header containing a "magic number" to identify the file type and version number, followed by the raw array of baseline_ap_t structs). Very efficient for reading and writing.
- Dirty Flag: The dirty flag is an optimization. The baseline is only written to the SD card if baseline_manager_save is called and the data in memory has actually changed. This reduces wear on the SD card.
- Data Aging: The baseline_manager_prune_old_entries function provides a mechanism to keep the baseline relevant by removing APs that are no longer in range (e.g., from a location you visited once).
Version 1
baseline.h
#ifndef BASELINE_H #define BASELINE_H #include "scanner.h" // We need the ap_info_t definition #include <furi.h> #include <storage/storage.h> // Define where the baseline database is stored on the SD card #define BASELINE_FILE_PATH EXT_PATH("apps_data/rogue_ap/legitimate_aps.dat") #define BASELINE_MAX_APS 100 // Maximum APs to store in the baseline /** * @brief Structure to hold persistent information for a single legitimate AP. */ typedef struct { uint8_t bssid[6]; // MAC address (Primary Key) char ssid[MAX_SSID_LEN + 1]; // Last known SSID WifiEncryptionType encryption; // Expected encryption // Fields for anomaly detection and data aging int8_t rssi_min; // Weakest signal seen int8_t rssi_max; // Strongest signal seen uint32_t last_seen_timestamp; // RTC timestamp of last sighting } baseline_ap_t; /** * @brief Main baseline manager state structure. */ typedef struct { baseline_ap_t* baseline_aps; // In-memory array of legitimate APs uint16_t count; // Current number of APs in the baseline uint16_t capacity; // Max capacity of the in-memory array FuriMutex* mutex; // Mutex for thread-safe access bool dirty; // Flag to track if the in-memory version has changed } BaselineManager; /** * @brief Allocates and initializes a new BaselineManager instance. * @param capacity The maximum number of baseline APs to hold in memory. * @return Pointer to the newly created BaselineManager. */ BaselineManager* baseline_manager_alloc(uint16_t capacity); /** * @brief Frees all resources associated with a BaselineManager instance. * @param manager Pointer to the BaselineManager instance to free. */ void baseline_manager_free(BaselineManager* manager); /** * @brief Loads the baseline database from the SD card into memory. * @param manager Pointer to the BaselineManager instance. * @return True on success or if no file exists, False on a read error. */ bool baseline_manager_load(BaselineManager* manager); /** * @brief Saves the in-memory baseline to the SD card, if it has changed. * @param manager Pointer to the BaselineManager instance. * @return True on success, False on a write error. */ bool baseline_manager_save(BaselineManager* manager); /** * @brief Updates the baseline with a newly scanned AP. * If the AP (by BSSID) is already in the baseline, its signal range and timestamp are updated. * If it's a new AP, it's added to the baseline. This is the core "learning" function. * @param manager Pointer to the BaselineManager instance. * @param ap_info Pointer to the AP information from the scanner. * @return True if the baseline was updated, False if it was full. */ bool baseline_manager_update(BaselineManager* manager, const ap_info_t* ap_info); /** * @brief Removes entries from the baseline that haven't been seen in a long time. * @param manager Pointer to the BaselineManager instance. * @param timeout_seconds The maximum age in seconds for an entry to be kept. * @return The number of entries that were pruned. */ uint16_t baseline_manager_prune_old_entries(BaselineManager* manager, uint32_t timeout_seconds); #endif // BASELINE_H
baseline.c
#include "baseline.h" #include <furi_hal_rtc.h> #include <flipper_format/flipper_format.h> // For file operations #include <toolbox/dir_walk.h> // To ensure directory exists // Header for our binary save file typedef struct { char magic[7]; // "RGFAP_B" uint8_t version; uint16_t count; } BaselineFileHeader; // --- Public Function Implementations --- BaselineManager* baseline_manager_alloc(uint16_t capacity) { BaselineManager* manager = malloc(sizeof(BaselineManager)); furi_check(manager != NULL); manager->baseline_aps = malloc(sizeof(baseline_ap_t) * capacity); furi_check(manager->baseline_aps != NULL); manager->capacity = capacity; manager->count = 0; manager->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); manager->dirty = false; return manager; } void baseline_manager_free(BaselineManager* manager) { if(!manager) return; furi_mutex_free(manager->mutex); free(manager->baseline_aps); free(manager); } bool baseline_manager_load(BaselineManager* manager) { furi_check(manager); Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); bool success = false; if(!storage_file_open(file, BASELINE_FILE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { // File doesn't exist, which is fine on first run. Return true. storage_file_free(file); furi_record_close(RECORD_STORAGE); return true; } if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) { storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); return false; } BaselineFileHeader header; if(storage_file_read(file, &header, sizeof(header)) != sizeof(header)) { goto cleanup; // Read error } if(strncmp(header.magic, "RGFAP_B", 7) != 0 || header.version != 1) { goto cleanup; // Invalid file format or version } manager->count = (header.count > manager->capacity) ? manager->capacity : header.count; if(storage_file_read(file, manager->baseline_aps, manager->count * sizeof(baseline_ap_t)) != manager->count * sizeof(baseline_ap_t)) { manager->count = 0; // Read error, clear data goto cleanup; } manager->dirty = false; success = true; cleanup: furi_mutex_release(manager->mutex); storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); return success; } bool baseline_manager_save(BaselineManager* manager) { furi_check(manager); if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return false; // Optimization: Don't write to SD card if nothing has changed if(!manager->dirty) { furi_mutex_release(manager->mutex); return true; } Storage* storage = furi_record_open(RECORD_STORAGE); // Ensure the directory exists storage_simply_mkdir(storage, furi_string_get_dirname(BASELINE_FILE_PATH)); File* file = storage_file_alloc(storage); bool success = false; if(!storage_file_open(file, BASELINE_FILE_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { goto cleanup; } BaselineFileHeader header = {.version = 1, .count = manager->count}; strncpy(header.magic, "RGFAP_B", 7); if(storage_file_write(file, &header, sizeof(header)) != sizeof(header)) { goto cleanup; } if(storage_file_write(file, manager->baseline_aps, manager->count * sizeof(baseline_ap_t)) != manager->count * sizeof(baseline_ap_t)) { goto cleanup; } manager->dirty = false; // Mark as clean after successful save success = true; cleanup: storage_file_close(file); storage_file_free(file); furi_record_close(RECORD_STORAGE); furi_mutex_release(manager->mutex); return success; } bool baseline_manager_update(BaselineManager* manager, const ap_info_t* ap_info) { furi_check(manager && ap_info); if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return false; bool updated = false; // Search for existing entry by BSSID for(uint16_t i = 0; i < manager->count; i++) { if(memcmp(manager->baseline_aps[i].bssid, ap_info->bssid, 6) == 0) { baseline_ap_t* entry = &manager->baseline_aps[i]; // Update RSSI range if(ap_info->rssi < entry->rssi_min) entry->rssi_min = ap_info->rssi; if(ap_info->rssi > entry->rssi_max) entry->rssi_max = ap_info->rssi; // Update timestamp entry->last_seen_timestamp = furi_hal_rtc_get_timestamp(); // Also update SSID and encryption in case they changed (e.g. guest network toggle) strncpy(entry->ssid, ap_info->ssid, MAX_SSID_LEN); entry->encryption = ap_info->encryption; manager->dirty = true; updated = true; goto exit_update; } } // If not found, add a new entry if there's space if(manager->count < manager->capacity) { baseline_ap_t* new_entry = &manager->baseline_aps[manager->count]; memcpy(new_entry->bssid, ap_info->bssid, 6); strncpy(new_entry->ssid, ap_info->ssid, MAX_SSID_LEN); new_entry->encryption = ap_info->encryption; new_entry->rssi_min = ap_info->rssi; new_entry->rssi_max = ap_info->rssi; new_entry->last_seen_timestamp = furi_hal_rtc_get_timestamp(); manager->count++; manager->dirty = true; updated = true; } exit_update: furi_mutex_release(manager->mutex); return updated; } uint16_t baseline_manager_prune_old_entries(BaselineManager* manager, uint32_t timeout_seconds) { furi_check(manager); if(furi_mutex_acquire(manager->mutex, FuriWaitForever) != FuriStatusOk) return 0; uint32_t current_time = furi_hal_rtc_get_timestamp(); uint16_t pruned_count = 0; // Iterate backwards to safely remove elements for(int i = manager->count - 1; i >= 0; i--) { if((current_time - manager->baseline_aps[i].last_seen_timestamp) > timeout_seconds) { // This entry is old. Remove it by shifting the rest of the array down. // This is only efficient if we don't prune many items at once. if(i < manager->count - 1) { memmove(&manager->baseline_aps[i], &manager->baseline_aps[i + 1], (manager->count - 1 - i) * sizeof(baseline_ap_t)); } manager->count--; manager->dirty = true; pruned_count++; } } furi_mutex_release(manager->mutex); return pruned_count; }