Rotate keys

This commit is contained in:
Kgothatso Ngako
2026-07-15 00:37:35 +02:00
parent 2477f77fb8
commit 4178e5e544
6 changed files with 194 additions and 66 deletions

View File

@@ -5,9 +5,16 @@ import androidx.room3.Query
import androidx.room3.Upsert
import at.torch.compose.database.model.MarmotKeyPackageBundle
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.flow.Flow
@Dao
interface MarmotKeyPackageBundleDao {
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey AND consumed = false AND rotated = false")
fun observeActiveMarmotKeyPackageBundle(publicKey: String): Flow<MarmotKeyPackageBundle?>
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey ORDER BY createdAt DESC")
fun observeAllMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>>
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey ORDER BY createdAt DESC")
fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>

View File

@@ -5,7 +5,6 @@ import at.torch.compose.database.model.MarmotInnerEvent
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.database.model.UnsignedNostrEvent
import at.torch.compose.extensions.toHex
import at.torch.compose.managers.MarmotInboundManager
import at.torch.compose.nostr.Relays
import at.torch.compose.repository.MarmotRepository
import co.touchlab.kermit.Logger
@@ -26,7 +25,6 @@ import com.vitorpamplona.quartz.marmot.mls.tree.Lifetime
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
@@ -95,6 +93,18 @@ class DatabaseMarmotRepository(
)
}
override suspend fun rotateMarmotKeyPackageBundle(marmotKeyPackageBundle: MarmotKeyPackageBundle) {
database.marmotKeyPackageBundleDao().upsert(
marmotKeyPackageBundle.copy(
rotated = true
)
)
}
override suspend fun observeMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>> {
return database.marmotKeyPackageBundleDao().observeAllMarmotKeyPackageBundles(publicKey)
}
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
return database.marmotKeyPackageBundleDao().getAllMarmotKeyPackageBundles(publicKey)
}
@@ -103,6 +113,10 @@ class DatabaseMarmotRepository(
return database.marmotInnerEventDao().observeUnprocessedMarmotInnerEvents(publicKey)
}
override suspend fun observeActiveMarmotKeyPackageBundle(publicKey: HexKey): Flow<MarmotKeyPackageBundle?> {
return database.marmotKeyPackageBundleDao().observeActiveMarmotKeyPackageBundle(publicKey)
}
override suspend fun encryptAndSendMarmotInnerEvent(
marmotInnerEvent: MarmotInnerEvent,
nostrSignerSync: NostrSignerSync

View File

@@ -13,10 +13,16 @@ interface MarmotRepository {
nsecPassword: String
)
suspend fun rotateMarmotKeyPackageBundle(marmotKeyPackageBundle: MarmotKeyPackageBundle)
suspend fun observeMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>>
suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<MarmotInnerEvent?>
suspend fun observeActiveMarmotKeyPackageBundle(publicKey: HexKey): Flow<MarmotKeyPackageBundle?>
suspend fun encryptAndSendMarmotInnerEvent(marmotInnerEvent: MarmotInnerEvent, nostrSignerSync: NostrSignerSync)
companion object {
@@ -28,6 +34,14 @@ interface MarmotRepository {
TODO("Not yet implemented")
}
override suspend fun rotateMarmotKeyPackageBundle(marmotKeyPackageBundle: MarmotKeyPackageBundle) {
TODO("Not yet implemented")
}
override suspend fun observeMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>> {
TODO("Not yet implemented")
}
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
return emptyList()
}
@@ -36,6 +50,10 @@ interface MarmotRepository {
TODO("Not yet implemented")
}
override suspend fun observeActiveMarmotKeyPackageBundle(publicKey: HexKey): Flow<MarmotKeyPackageBundle?> {
TODO("Not yet implemented")
}
override suspend fun encryptAndSendMarmotInnerEvent(
marmotInnerEvent: MarmotInnerEvent,
nostrSignerSync: NostrSignerSync

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Bento
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Hub
import androidx.compose.material.icons.filled.ImportExport
@@ -23,11 +24,16 @@ import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.PrivateConnectivity
import androidx.compose.material.icons.filled.Publish
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.filled.Unpublished
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.VerifiedUser
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -38,9 +44,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.extensions.hexToNpubHrp
import at.torch.compose.extensions.toFormattedTimeAndDateString
import at.torch.compose.repository.MarmotRepository
@@ -54,6 +62,7 @@ import at.torch.compose.ui.view.model.KeyPackageManagementViewModel
import at.torch.compose.ui.view.state.KeyPackageManagementUIState
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import fr.acinq.phoenix.preferredAmountUnit
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -147,28 +156,67 @@ fun KeyPackageManagementScreen(
items = keyPackageManagementUIState.keyPackageBundles
) { marmotKeyPackageBundle ->
Row {
Text(
text = marmotKeyPackageBundle.id,
style = MaterialTheme.typography.bodySmall
)
Text(
text = marmotKeyPackageBundle.createdAt.toFormattedTimeAndDateString(),
style = MaterialTheme.typography.labelSmall
)
TextButton(
onClick = {
onNavigateToRoute.invoke(
ImplementationPendingRoute("Delete Key Package")
Card {
ListItem(
leadingContent = {
if (marmotKeyPackageBundle.rotated) {
Icon(
Icons.Default.Unpublished,
contentDescription = "Key package rotated"
)
} else if (marmotKeyPackageBundle.consumed) {
Icon(
Icons.Default.DoneAll,
contentDescription = "Consumed key package event"
)
} else {
// TODO: Get broadcast status of key package...
Icon(
Icons.Default.VerifiedUser,
contentDescription = "Key package published"
)
}
},
headlineContent = {
Text(
text = marmotKeyPackageBundle.id,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis
)
},
supportingContent = {
Row(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = marmotKeyPackageBundle.createdAt.toFormattedTimeAndDateString(),
style = MaterialTheme.typography.labelSmall
)
Spacer(
modifier = Modifier.weight(1f)
)
}
},
trailingContent = {
if (marmotKeyPackageBundle.rotated.not() && marmotKeyPackageBundle.consumed.not()) {
IconButton(
onClick = {
keyPackageManagementViewModel.rotateKeyPackage(
marmotKeyPackageBundle
)
}
) {
Icon(
Icons.Default.LockReset,
contentDescription = "Rotate Key"
)
}
}
}
) {
Text(
"Delete"
)
}
)
}
}
}
@@ -177,10 +225,6 @@ fun KeyPackageManagementScreen(
}
KeyPackageManagementUIState.Loading -> {
LoadingDataIndicator()
LaunchedEffect(true) {
keyPackageManagementViewModel.loadNostrFeed()
}
}
KeyPackageManagementUIState.NotFound -> {
Column(
@@ -203,7 +247,17 @@ private fun UnannouncedProfileScreenPreview() {
) {
KeyPackageManagementScreen(
initialActiveProfileUIState = KeyPackageManagementUIState.Loaded(
keyPackageBundles = emptyList()
keyPackageBundles = listOf(
MarmotKeyPackageBundle(
id = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
publicKey = "",
tlsEncodedMarmotKeyPackage = ByteArray(0),
ncryptsecInitPrivateKey = "",
ncryptsecEncryptionPrivateKey = "",
ncryptsecSignaturePrivateKey = "",
consumed = true
)
)
),
activeUserPublicKey = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
nostrEventId = "",

View File

@@ -8,11 +8,13 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.repository.MarmotRepository
import at.torch.compose.ui.view.state.KeyPackageManagementUIState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
class KeyPackageManagementViewModel(
@@ -24,16 +26,17 @@ class KeyPackageManagementViewModel(
var keyPackageManagementUIState: KeyPackageManagementUIState by mutableStateOf(initialActiveProfileUIState)
private set
fun retryLoad() {
keyPackageManagementUIState = KeyPackageManagementUIState.Loading
init {
loadNostrFeed()
}
fun loadNostrFeed() {
viewModelScope.launch(Dispatchers.IO) {
keyPackageManagementUIState = KeyPackageManagementUIState.Loaded(
keyPackageBundles = marmotRepository.getMarmotKeyPackageBundles(activeUserPublicKey)
)
marmotRepository.observeMarmotKeyPackageBundles(activeUserPublicKey).distinctUntilChanged().collect { keyPackageBundles ->
keyPackageManagementUIState = KeyPackageManagementUIState.Loaded(
keyPackageBundles = keyPackageBundles
)
}
}
}
@@ -50,6 +53,12 @@ class KeyPackageManagementViewModel(
}
}
fun rotateKeyPackage(marmotKeyPackageBundle: MarmotKeyPackageBundle) {
viewModelScope.launch(Dispatchers.IO) {
marmotRepository.rotateMarmotKeyPackageBundle(marmotKeyPackageBundle)
}
}
companion object {
const val TAG = "KeyPackageManagementViewModel"

View File

@@ -68,6 +68,7 @@ class NotaryViewModel(
observeUnsignedNostrEvents(keyPair)
observeUnsealedGiftWrapPayloads(keyPair)
observeUnprocessedMarmotInnerEvents(keyPair)
observeActiveMarmotKeyPackageBundle(keyPair)
}
}
}
@@ -77,45 +78,45 @@ class NotaryViewModel(
private fun observeUnsignedNostrEvents(
keyPair: KeyPair
) {
val tempSigner = NostrSignerSync(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents: ${keyPair.pubKey.toHexKey()}" }
nostrRepository.observeUnsignedNostrEvents(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
scope.launch(Dispatchers.IO) {
unsignedNostrEventOrNull?.let { unsignedNostrEvent ->
logger.d("Unsigned: ${unsignedNostrEvent.kind}")
val event = tempSigner.signNormal<Event>(
createdAt = unsignedNostrEvent.createdAt.epochSeconds,
kind = unsignedNostrEvent.kind,
tags = unsignedNostrEvent.tags,
content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content
)
logger.d("Signed: ${event.toJson()}")
val tempSigner = NostrSignerSync(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents: ${keyPair.pubKey.toHexKey()}" }
nostrRepository.observeUnsignedNostrEvents(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
scope.launch(Dispatchers.IO) {
unsignedNostrEventOrNull?.let { unsignedNostrEvent ->
logger.d("Unsigned: ${unsignedNostrEvent.kind}")
val event = tempSigner.signNormal<Event>(
createdAt = unsignedNostrEvent.createdAt.epochSeconds,
kind = unsignedNostrEvent.kind,
tags = unsignedNostrEvent.tags,
content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content
)
logger.d("Signed: ${event.toJson()}")
nostrRepository.publishNostrEvent(
unsignedNostrEvent,
_root_ide_package_.at.torch.compose.database.model.NostrEvent(
id = event.id,
pubKey = event.pubKey,
kind = event.kind,
tags = event.tags,
content = event.content,
createdAt = Instant.fromEpochSeconds(event.createdAt),
sig = event.sig,
unsignedNostrEventId = unsignedNostrEvent.id
),
relayURLs = at.torch.compose.nostr.Relays.DefaultDMRelayList.map { normalizedRelayUrl -> normalizedRelayUrl.url },
activeKeyPair = keyPair
)
}
nostrRepository.publishNostrEvent(
unsignedNostrEvent,
_root_ide_package_.at.torch.compose.database.model.NostrEvent(
id = event.id,
pubKey = event.pubKey,
kind = event.kind,
tags = event.tags,
content = event.content,
createdAt = Instant.fromEpochSeconds(event.createdAt),
sig = event.sig,
unsignedNostrEventId = unsignedNostrEvent.id
),
relayURLs = at.torch.compose.nostr.Relays.DefaultDMRelayList.map { normalizedRelayUrl -> normalizedRelayUrl.url },
activeKeyPair = keyPair
)
}
}
}
}
}
private fun observeUnsealedGiftWrapPayloads(
@@ -168,4 +169,29 @@ class NotaryViewModel(
}
}
private fun observeActiveMarmotKeyPackageBundle(
keyPair: KeyPair
) {
val tempSigner = NostrSignerSync(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeActiveMarmotKeyPackageBundle: ${keyPair.pubKey.toHexKey()}" }
marmotRepository.observeActiveMarmotKeyPackageBundle(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { marmotKeyPackageBundleOrNull ->
scope.launch(Dispatchers.IO) {
if (marmotKeyPackageBundleOrNull == null) {
logger.d("Need to create new key package bundle")
marmotRepository.publishMarmotKeyPackageBundle(
publicKey = keyPair.pubKey.toHexKey(),
nsecPassword = "" // TODO: Implement nsecPassword logic...
)
}
}
}
}
}
}