From 463141a7664fcf1f0ec48b614fb1ba624d884c73 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 6 Jul 2026 14:47:11 +0200 Subject: [PATCH] Handle retained secrets --- .../torch/compose/database/TorchDatabase.kt | 3 +- .../compose/database/dao/MarmotOutboundDao.kt | 62 ++++++++++++++----- .../dao/MarmotRetainedEpochSecretDao.kt | 21 ++++--- .../model/MarmotRetainedEpochSecret.kt | 35 +++-------- 4 files changed, 72 insertions(+), 49 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt index 1194e8a5..87d8c96b 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt @@ -160,8 +160,7 @@ abstract class TorchDatabase: RoomDatabase() { abstract fun marmotOutboundDao(): MarmotOutboundDao - abstract fun marmotRetainedEpochSecret(): MarmotRetainedEpochSecretDao - + abstract fun marmotRetainedEpochSecretDao(): MarmotRetainedEpochSecretDao abstract fun mentionDao(): MentionDao diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotOutboundDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotOutboundDao.kt index 25d61813..057216db 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotOutboundDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotOutboundDao.kt @@ -14,10 +14,13 @@ import at.torch.compose.database.model.MarmotGroupEvent import at.torch.compose.database.model.MarmotInnerEvent import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration import at.torch.compose.database.model.MarmotKeyPackage +import at.torch.compose.database.model.MarmotRetainedEpochSecret import at.torch.compose.database.model.NostrEvent +import at.torch.compose.database.model.Participant import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.extensions.exporterSecret import at.torch.compose.extensions.toHex +import at.torch.compose.managers.MarmotInboundManager.EPOCH_RETENTION_WINDOW import at.torch.compose.nostr.Relays import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData @@ -56,12 +59,12 @@ abstract class MarmotOutboundDao( peerKeyPackage: MarmotKeyPackage, relays: List = Relays.DefaultDMRelayList.map { it.url } ): String { - val groupId = RandomInstance.bytes(32).toHexKey() + val nostrGroupId = RandomInstance.bytes(32).toHexKey() val mlsGroup = MlsGroup.create( identity = userPublicKey.hexToByteArray(), initialExtensions = listOf( MarmotGroupData.bootstrap( - nostrGroupId = groupId, + nostrGroupId = nostrGroupId, creatorPubKey = userPublicKey, outboxRelays = relays, ).toExtension() @@ -70,7 +73,7 @@ abstract class MarmotOutboundDao( // Save Chat Room val chatRoom = ChatRoom( - id = groupId, + id = nostrGroupId, userPublicKey = userPublicKey, mlsGroupState = mlsGroup.saveState().encodeTls().toHex(), subject = null, @@ -79,11 +82,25 @@ abstract class MarmotOutboundDao( chatRoom ) - // TODO: Save Participants... + // Save Participants... + database.participantDao().upsert( + listOf( + Participant( + participantPublicKey = userPublicKey, + chatRoomId = chatRoom.id, + relayHint = relays.first() + ), + Participant( + participantPublicKey = peerPublicKey, + chatRoomId = chatRoom.id, + relayHint = relays.first() + ) + ) + ) // Invite peer... inviteMember( - groupId = groupId, + nostrGroupId = nostrGroupId, mlsGroup = mlsGroup, userPublicKey = userPublicKey, peerPublicKey = peerPublicKey, @@ -99,11 +116,11 @@ abstract class MarmotOutboundDao( ) ) - return groupId + return nostrGroupId } private suspend fun inviteMember( - groupId: HexKey, + nostrGroupId: HexKey, mlsGroup: MlsGroup, userPublicKey: HexKey, peerPublicKey: HexKey, @@ -130,9 +147,25 @@ abstract class MarmotOutboundDao( peerKeyPackage.tlsEncodedMarmotKeyPackage ) + database.marmotRetainedEpochSecretDao().insert( + MarmotRetainedEpochSecret( + chatRoomId = nostrGroupId, + epoch = retainedBefore.epoch, + senderDataSecret = retainedBefore.senderDataSecret, + encryptionSecret = retainedBefore.encryptionSecret, + leafCount = retainedBefore.leafCount, + exporterSecret = retainedBefore.exporterSecret, + ) + ) -// pushRetainedEpoch(nostrGroupId, retainedBefore) -// persistGroup(nostrGroupId) + val defenestratableMarmotRetainedEpochSecrets = database.marmotRetainedEpochSecretDao() + .defenestratableMarmotRetainedEpochSecret( + epochCutOffPoint = retainedBefore.epoch - EPOCH_RETENTION_WINDOW.toLong() + ) + logger.d("Defenestratable Retained Epochs: ${defenestratableMarmotRetainedEpochSecrets.map { it.epoch }}") + database.marmotRetainedEpochSecretDao().defenestrate( + defenestratableMarmotRetainedEpochSecrets + ) // CommitEvent... val encryptedContent = encryptedCommitEvent( @@ -143,13 +176,12 @@ abstract class MarmotOutboundDao( val template = GroupEvent.build( encryptedContentBase64 = encryptedContent, - nostrGroupId = groupId + nostrGroupId = nostrGroupId ) val ephemeralSigner = NostrSignerInternal(KeyPair()) val commitEvent: GroupEvent = ephemeralSigner.sign(template) - database.nostrEventDao().upsert( NostrEvent( id = commitEvent.id, @@ -167,7 +199,7 @@ abstract class MarmotOutboundDao( MarmotCommitResult( id = commitEvent.id, isOneMemberInitialGroupCreation = isOneMemberInitialGroupCreation, - chatRoomId = groupId, + chatRoomId = nostrGroupId, commitBytes = commitResult.commitBytes, preCommitExporterSecret = commitResult.preCommitExporterSecret, welcomeBytes = commitResult.welcomeBytes, @@ -199,7 +231,7 @@ abstract class MarmotOutboundDao( welcomeBase64 = welcomeBase64, keyPackageEventId = peerKeyPackage.id, relays = relays.map { NormalizedRelayUrl(it) }, - nostrGroupId = groupId, + nostrGroupId = nostrGroupId, ) // Compute welcomeEventId val welcomeEventId = EventHasher.hashId( @@ -213,7 +245,7 @@ abstract class MarmotOutboundDao( GiftWrapPayload( id = welcomeEventId, publicKey = userPublicKey, - chatRoomId = groupId, + chatRoomId = nostrGroupId, kind = welcomeEvent.kind, tags = welcomeEvent.tags, content = welcomeEvent.content, @@ -224,7 +256,7 @@ abstract class MarmotOutboundDao( database.chatMessageDao().upsert( ChatMessage( content = "Invited participant to chat", // use profile.humanReadable... - chatRoomId = groupId, + chatRoomId = nostrGroupId, senderPublicKey = userPublicKey, isUserMessage = true, // TODO: This is information message... giftWrapPayloadId = welcomeEventId, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotRetainedEpochSecretDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotRetainedEpochSecretDao.kt index 342b888b..3d6459d4 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotRetainedEpochSecretDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotRetainedEpochSecretDao.kt @@ -1,16 +1,23 @@ package at.torch.compose.database.dao import androidx.room3.Dao +import androidx.room3.Delete +import androidx.room3.Insert +import androidx.room3.OnConflictStrategy.Companion.IGNORE import androidx.room3.Query -import androidx.room3.Upsert -import at.torch.compose.database.model.MarmotCommitResult -import at.torch.compose.database.model.MarmotKeyPackage import at.torch.compose.database.model.MarmotRetainedEpochSecret -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import fr.acinq.bitcoin.PublicKey @Dao interface MarmotRetainedEpochSecretDao { - @Upsert - suspend fun upsert(marmotRetainedEpochSecret: MarmotRetainedEpochSecret) + @Query("SELECT * FROM MarmotRetainedEpochSecret WHERE epoch < :epochCutOffPoint") + suspend fun defenestratableMarmotRetainedEpochSecret(epochCutOffPoint: Long): List + + @Insert( + onConflict = IGNORE + ) + suspend fun insert(marmotRetainedEpochSecret: MarmotRetainedEpochSecret) + + @Delete + suspend fun defenestrate(marmotRetainedEpochSecrets: List) + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotRetainedEpochSecret.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotRetainedEpochSecret.kt index a757227d..8870bc72 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotRetainedEpochSecret.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotRetainedEpochSecret.kt @@ -2,6 +2,12 @@ package at.torch.compose.database.model import androidx.room3.Entity import androidx.room3.ForeignKey +import at.torch.compose.database.model.traits.LocalStoreEntity +import at.torch.compose.database.model.traits.SoftDeletableEntity +import at.torch.compose.database.model.traits.TimestampedEntity +import at.torch.compose.database.model.traits.UserViewableEntity +import kotlin.time.Clock +import kotlin.time.Instant @Entity( foreignKeys = [ @@ -21,31 +27,10 @@ data class MarmotRetainedEpochSecret( val encryptionSecret: ByteArray, // TODO: Encrypt this... val leafCount: Int, val exporterSecret: ByteArray = ByteArray(0), // TODO: Encrypt this... -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as MarmotRetainedEpochSecret - - if (epoch != other.epoch) return false - if (leafCount != other.leafCount) return false - if (chatRoomId != other.chatRoomId) return false - if (!senderDataSecret.contentEquals(other.senderDataSecret)) return false - if (!encryptionSecret.contentEquals(other.encryptionSecret)) return false - if (!exporterSecret.contentEquals(other.exporterSecret)) return false - - return true - } - - override fun hashCode(): Int { - var result = epoch.hashCode() - result = 31 * result + leafCount - result = 31 * result + chatRoomId.hashCode() - result = 31 * result + senderDataSecret.contentHashCode() - result = 31 * result + encryptionSecret.contentHashCode() - result = 31 * result + exporterSecret.contentHashCode() - return result - } + override val createdAt: Instant = Clock.System.now(), + override val updatedAt: Instant = createdAt, + override val savedAt: Instant = Clock.System.now(), +): TimestampedEntity, LocalStoreEntity { } \ No newline at end of file