fix(marmot): add diagnostic logs to group persistence path + use explicit AndroidKeyStore provider
User-reported issue: creating a Marmot group works in a single session,
but the group is gone after app restart. The store code path survives
the persistGroup save on creation, but something between save and
restore silently loses the state.
Changes:
- KeyStoreEncryption: pass "AndroidKeyStore" explicitly to
KeyGenerator.getInstance so generator.init(KeyGenParameterSpec) is
guaranteed to land on the AndroidKeyStore provider (the default
provider selection is not guaranteed to choose it). Also wrap
encrypt/decrypt in try/catch so the real exception is logged
instead of bubbling up as a generic failure.
- AndroidMlsGroupStateStore: log rootDir at construction, and log
every save/load/listGroups/delete with file paths, byte counts,
and whether the target file actually exists after writing. Any
thrown exception is now logged with full stack trace before
rethrowing.
- MlsGroupManager: log create / persist / restoreAll with group ids,
byte counts, and in-memory group count so we can see exactly which
step drops state. The existing "corrupted state, delete" branch now
logs at ERROR with a clearer marker so it's easy to spot in the logs.
- MarmotManager + AccountCacheState: log which MLS store implementation
is chosen per account (Android vs InMemory fallback) and the root
filesDir path, plus restoreAll() begin/end with restored group ids.
These logs should make it obvious whether:
1. persistence is silently falling back to InMemoryMlsGroupStateStore,
2. save is actually writing the file to disk,
3. listGroups is finding the directory on restart,
4. decrypt is throwing and the restoreAll catch block is wiping
the "corrupted" state.
https://claude.ai/code/session_014EKS8JBwSpap34aM6FnYLm
This commit is contained in:
+40
-16
@@ -105,11 +105,17 @@ class MlsGroupManager(
|
||||
suspend fun restoreAll() =
|
||||
mutex.withLock {
|
||||
val groupIds = store.listGroups()
|
||||
Log.d(TAG) { "restoreAll(): store reports ${groupIds.size} groups: $groupIds" }
|
||||
for (nostrGroupId in groupIds) {
|
||||
try {
|
||||
val stateBytes = store.load(nostrGroupId) ?: continue
|
||||
val stateBytes = store.load(nostrGroupId)
|
||||
if (stateBytes == null) {
|
||||
Log.w(TAG) { "restoreAll(): load returned null for $nostrGroupId, skipping" }
|
||||
continue
|
||||
}
|
||||
val state = MlsGroupState.decodeTls(stateBytes)
|
||||
groups[nostrGroupId] = MlsGroup.restore(state)
|
||||
Log.d(TAG) { "restoreAll(): restored group $nostrGroupId (${stateBytes.size} bytes)" }
|
||||
|
||||
// Restore retained epochs
|
||||
val retained = store.loadRetainedEpochs(nostrGroupId)
|
||||
@@ -118,17 +124,19 @@ class MlsGroupManager(
|
||||
retained
|
||||
.map { RetainedEpochSecrets.decodeTls(TlsReader(it)) }
|
||||
.toMutableList()
|
||||
Log.d(TAG) { "restoreAll(): restored ${retained.size} retained epochs for $nostrGroupId" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Corrupted state — log and remove it so it doesn't block future joins
|
||||
Log.e(
|
||||
"MlsGroupManager",
|
||||
"Corrupted state for group $nostrGroupId, deleting: ${e.message}",
|
||||
TAG,
|
||||
"restoreAll(): Corrupted state for group $nostrGroupId, DELETING: ${e.message}",
|
||||
e,
|
||||
)
|
||||
store.delete(nostrGroupId)
|
||||
}
|
||||
}
|
||||
Log.d(TAG) { "restoreAll(): finished with ${groups.size} active groups in memory" }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,9 +170,11 @@ class MlsGroupManager(
|
||||
signingKey: ByteArray? = null,
|
||||
): MlsGroup =
|
||||
mutex.withLock {
|
||||
Log.d(TAG) { "createGroup($nostrGroupId): creating new MLS group" }
|
||||
val group = MlsGroup.create(identity, signingKey)
|
||||
groups[nostrGroupId] = group
|
||||
persistGroup(nostrGroupId)
|
||||
Log.d(TAG) { "createGroup($nostrGroupId): done, in-memory group count=${groups.size}" }
|
||||
group
|
||||
}
|
||||
|
||||
@@ -467,20 +477,32 @@ class MlsGroupManager(
|
||||
?: throw IllegalStateException("Not a member of group $nostrGroupId")
|
||||
|
||||
private suspend fun persistGroup(nostrGroupId: HexKey) {
|
||||
val group = groups[nostrGroupId] ?: return
|
||||
val state = group.saveState()
|
||||
store.save(nostrGroupId, state.encodeTls())
|
||||
val group = groups[nostrGroupId]
|
||||
if (group == null) {
|
||||
Log.w(TAG) { "persistGroup($nostrGroupId): group not in memory, skipping" }
|
||||
return
|
||||
}
|
||||
try {
|
||||
val state = group.saveState()
|
||||
val encoded = state.encodeTls()
|
||||
Log.d(TAG) { "persistGroup($nostrGroupId): serialized ${encoded.size} bytes, calling store.save" }
|
||||
store.save(nostrGroupId, encoded)
|
||||
|
||||
// Also persist retained epochs
|
||||
val retained = retainedEpochs[nostrGroupId]
|
||||
if (retained != null) {
|
||||
val retainedBytes =
|
||||
retained.map { epoch ->
|
||||
val writer = TlsWriter()
|
||||
epoch.encodeTls(writer)
|
||||
writer.toByteArray()
|
||||
}
|
||||
store.saveRetainedEpochs(nostrGroupId, retainedBytes)
|
||||
// Also persist retained epochs
|
||||
val retained = retainedEpochs[nostrGroupId]
|
||||
if (retained != null) {
|
||||
val retainedBytes =
|
||||
retained.map { epoch ->
|
||||
val writer = TlsWriter()
|
||||
epoch.encodeTls(writer)
|
||||
writer.toByteArray()
|
||||
}
|
||||
store.saveRetainedEpochs(nostrGroupId, retainedBytes)
|
||||
Log.d(TAG) { "persistGroup($nostrGroupId): persisted ${retainedBytes.size} retained epochs" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "persistGroup($nostrGroupId) FAILED: ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +603,8 @@ class MlsGroupManager(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MlsGroupManager"
|
||||
|
||||
/**
|
||||
* Number of past epochs to retain for late-arriving message decryption.
|
||||
* MLS forward secrecy guarantees mean we want to limit this window.
|
||||
|
||||
Reference in New Issue
Block a user