refactor(#194): key channel history by PSK identity + migration

Channel message history was persisted keyed by the reusable slot index, so a
channel added to a slot that once held another channel inherited its messages.

Storage now keys by the channel's PSK identity via a resolver on the store
(index -> channel PSK), leaving all call sites unchanged. A one-time migration
adopts pre-existing slot-index history into the PSK key on first load (only when
the PSK is known); clearChannelMessages now wipes both the PSK key and the raw
slot-index key so Build-B's reuse-clear can't be re-migrated onto a new occupant.

Build-A (durable) of epic #190; supersedes Build-B's (#193) guard. Also removes
the dead device-wide fallback read in loadChannelMessages.
pull/202/head
Strycher 2 weeks ago
parent 98841f1c2b
commit 7da6e2cde2

@ -4193,6 +4193,10 @@ class MeshCoreConnector extends ChangeNotifier {
//set all the stores' public key so they can load the correct data
_channelMessageStore.setPublicKeyHex = selfPublicKeyHex;
// Key channel history by the channel's PSK identity (not its reusable slot
// index) so reusing a slot can't surface a previous occupant's messages. (#194)
_channelMessageStore.channelPskResolver = (index) =>
_findChannelByIndex(index)?.pskHex;
_messageStore.setPublicKeyHex = selfPublicKeyHex;
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;

@ -10,12 +10,36 @@ import 'prefs_manager.dart';
class ChannelMessageStore {
static const String _keyPrefix = 'channel_messages_';
/// Marks a PSK-identity key so it can never collide with a legacy slot-index
/// key (a PSK is 32 hex chars; an index is a small integer). (#194)
static const String _pskMarker = 'psk_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
/// Resolves a channel slot index to its channel's PSK hex — a stable identity
/// that does not change when the channel moves slots. Set by the connector.
/// When it yields a non-empty hex, history is keyed by PSK so reusing a slot
/// can never surface a previous occupant's messages. Falls back to the slot
/// index only while the PSK is unknown (channel not yet synced). (#194)
String? Function(int index)? channelPskResolver;
String get keyFor => '$_keyPrefix$publicKeyHex';
/// Device-scoped slot-index key (the pre-#194 scheme; still used as a
/// migration source and as the fallback when the PSK is unknown).
String _indexKey(int channelIndex) => '$keyFor$channelIndex';
/// Active storage key: PSK identity when known, else the slot index.
String _storageKey(int channelIndex) {
final pskHex = channelPskResolver?.call(channelIndex);
if (pskHex != null && pskHex.isNotEmpty) {
return '$keyFor$_pskMarker$pskHex';
}
return _indexKey(channelIndex);
}
/// Save messages for a specific channel
Future<void> saveChannelMessages(
int channelIndex,
@ -28,13 +52,8 @@ class ChannelMessageStore {
return;
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
// Convert messages to JSON
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
final jsonString = jsonEncode(jsonList);
await prefs.setString(key, jsonString);
await prefs.setString(_storageKey(channelIndex), jsonEncode(jsonList));
}
/// Load messages for a specific channel
@ -46,24 +65,31 @@ class ChannelMessageStore {
return [];
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
final key = _storageKey(channelIndex);
String? jsonString = prefs.getString(key);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(oldKey);
prefs.remove(oldKey);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
// One-time migration into the PSK-identity key. Only runs when the PSK is
// known (key != index key). Adopts pre-#194 history keyed by slot index
// device-scoped first, then the oldest unscoped key and drops the source.
// Build-B (#193) clears a slot's index history on mismatched reuse, so a
// live slot's index data is the channel's own by the time we read here.
if ((jsonString == null || jsonString.isEmpty) &&
key != _indexKey(channelIndex)) {
for (final legacyKey in [
_indexKey(channelIndex),
'$_keyPrefix$channelIndex', // pre-device-scoping, unscoped index key
]) {
final legacy = prefs.getString(legacyKey);
if (legacy != null && legacy.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $oldKey to scoped key $key',
'Migrating channel messages $legacyKey -> $key (PSK-keyed, #194)',
);
await prefs.setString(key, legacyJsonString);
jsonString = legacyJsonString;
await prefs.setString(key, legacy);
await prefs.remove(legacyKey);
jsonString = legacy;
break;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
@ -72,22 +98,25 @@ class ChannelMessageStore {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList.map((json) => _messageFromJson(json)).toList();
} catch (e) {
// If parsing fails, return empty list
appLogger.error('Failed to parse channel messages for $key: $e');
return [];
}
}
/// Clear messages for a specific channel
/// Clear messages for a specific channel slot. Removes BOTH the PSK-identity
/// key and the raw slot-index key: deleteChannel needs the channel's own
/// history gone, and setChannel's reuse-clear (#193) needs any stale
/// slot-index history gone so it can't be migrated onto the new occupant.
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
await prefs.remove(key);
await prefs.remove(_storageKey(channelIndex));
await prefs.remove(_indexKey(channelIndex));
}
/// Clear all channel messages
Future<void> clearAllChannelMessages() async {
final prefs = PrefsManager.instance;
final keys = prefs.getKeys().where((k) => k.startsWith(keyFor));
final keys = prefs.getKeys().where((k) => k.startsWith(keyFor)).toList();
for (var key in keys) {
await prefs.remove(key);
}

Loading…
Cancel
Save

Powered by TurnKey Linux.