Rotate keys
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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...
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user