Save profile keypackage bundle

This commit is contained in:
Kgothatso Ngako
2026-06-19 10:51:39 +03:00
parent 24cb4db9a9
commit 3b9be3a9dd
13 changed files with 281 additions and 51 deletions

View File

@@ -5,45 +5,50 @@ import androidx.room3.RoomDatabase
import androidx.room3.TypeConverters
import androidx.room3.immediateTransaction
import androidx.room3.useWriterConnection
import at.torch.compose.database.dao.MarmotKeyPackageBundleDao
import at.torch.compose.database.dao.MarmotKeyPackageDao
import at.torch.compose.database.model.MarmotKeyPackageBundle
import kotlin.time.Instant
val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
@Database(
entities = [
_root_ide_package_.at.torch.compose.database.model.BroadcastNostrEventReceipt::class,
_root_ide_package_.at.torch.compose.database.model.BroadcastNostrEventRequest::class,
_root_ide_package_.at.torch.compose.database.model.Connection::class,
_root_ide_package_.at.torch.compose.database.model.ChatMessage::class,
_root_ide_package_.at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation::class,
_root_ide_package_.at.torch.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation::class,
_root_ide_package_.at.torch.compose.database.model.ChatMessageNostrEventRelation::class,
_root_ide_package_.at.torch.compose.database.model.ChatRoom::class,
_root_ide_package_.at.torch.compose.database.model.InReplyToRelation::class,
_root_ide_package_.at.torch.compose.database.model.GiftWrapMessage::class,
_root_ide_package_.at.torch.compose.database.model.GiftWrapSeal::class,
_root_ide_package_.at.torch.compose.database.model.GiftWrapPayload::class,
_root_ide_package_.at.torch.compose.database.model.Mention::class,
_root_ide_package_.at.torch.compose.database.model.NegentropySynchronizeRequest::class,
_root_ide_package_.at.torch.compose.database.model.NegentropySynchronizeResult::class,
_root_ide_package_.at.torch.compose.database.model.NostrEvent::class,
_root_ide_package_.at.torch.compose.database.model.NostrEventRelay::class,
_root_ide_package_.at.torch.compose.database.model.Participant::class,
_root_ide_package_.at.torch.compose.database.model.Post::class,
_root_ide_package_.at.torch.compose.database.model.Profile::class,
_root_ide_package_.at.torch.compose.database.model.QuotedRelation::class,
_root_ide_package_.at.torch.compose.database.model.Reaction::class,
_root_ide_package_.at.torch.compose.database.model.RecentSearch::class,
_root_ide_package_.at.torch.compose.database.model.Relay::class,
_root_ide_package_.at.torch.compose.database.model.RepostedRelation::class,
_root_ide_package_.at.torch.compose.database.model.SynchronizeNostrEventRequest::class,
_root_ide_package_.at.torch.compose.database.model.SynchronizeNostrEventResult::class,
_root_ide_package_.at.torch.compose.database.model.UnsignedNostrEvent::class,
_root_ide_package_.at.torch.compose.database.model.Zap::class
at.torch.compose.database.model.BroadcastNostrEventReceipt::class,
at.torch.compose.database.model.BroadcastNostrEventRequest::class,
at.torch.compose.database.model.Connection::class,
at.torch.compose.database.model.ChatMessage::class,
at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation::class,
at.torch.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation::class,
at.torch.compose.database.model.ChatMessageNostrEventRelation::class,
at.torch.compose.database.model.ChatRoom::class,
at.torch.compose.database.model.GiftWrapMessage::class,
at.torch.compose.database.model.GiftWrapSeal::class,
at.torch.compose.database.model.GiftWrapPayload::class,
at.torch.compose.database.model.InReplyToRelation::class,
at.torch.compose.database.model.MarmotKeyPackage::class,
at.torch.compose.database.model.MarmotKeyPackageBundle::class,
at.torch.compose.database.model.Mention::class,
at.torch.compose.database.model.NegentropySynchronizeRequest::class,
at.torch.compose.database.model.NegentropySynchronizeResult::class,
at.torch.compose.database.model.NostrEvent::class,
at.torch.compose.database.model.NostrEventRelay::class,
at.torch.compose.database.model.Participant::class,
at.torch.compose.database.model.Post::class,
at.torch.compose.database.model.Profile::class,
at.torch.compose.database.model.QuotedRelation::class,
at.torch.compose.database.model.Reaction::class,
at.torch.compose.database.model.RecentSearch::class,
at.torch.compose.database.model.Relay::class,
at.torch.compose.database.model.RepostedRelation::class,
at.torch.compose.database.model.SynchronizeNostrEventRequest::class,
at.torch.compose.database.model.SynchronizeNostrEventResult::class,
at.torch.compose.database.model.UnsignedNostrEvent::class,
at.torch.compose.database.model.Zap::class
],
version = 1
)
@TypeConverters(_root_ide_package_.at.torch.compose.database.converters.AuxConverters::class)
@TypeConverters(at.torch.compose.database.converters.AuxConverters::class)
abstract class TorchDatabase: RoomDatabase() {
abstract fun broadcastNostrEventReceiptDao(): at.torch.compose.database.dao.BroadcastNostrEventReceiptDao
abstract fun broadcastNostrEventRequestDao(): at.torch.compose.database.dao.BroadcastNostrEventRequestDao
@@ -68,6 +73,10 @@ abstract class TorchDatabase: RoomDatabase() {
abstract fun inReplyToRelationDao(): at.torch.compose.database.dao.InReplyToRelationDao
abstract fun marmotKeyPackageDao(): MarmotKeyPackageDao
abstract fun marmotKeyPackageBundleDao(): MarmotKeyPackageBundleDao
abstract fun mentionDao(): at.torch.compose.database.dao.MentionDao
abstract fun negentropySynchronizeRequestDao(): at.torch.compose.database.dao.NegentropySynchronizeRequestDao

View File

@@ -0,0 +1,15 @@
package at.torch.compose.database.dao
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.OnConflictStrategy.Companion.IGNORE
import androidx.room3.Upsert
import at.torch.compose.database.model.MarmotKeyPackage
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.database.model.Mention
@Dao
interface MarmotKeyPackageBundleDao {
@Upsert
suspend fun upsert(marmotKeyPackageBundle: MarmotKeyPackageBundle)
}

View File

@@ -0,0 +1,15 @@
package at.torch.compose.database.dao
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.OnConflictStrategy.Companion.IGNORE
import androidx.room3.Upsert
import at.torch.compose.database.model.MarmotKeyPackage
import at.torch.compose.database.model.Mention
@Dao
interface MarmotKeyPackageDao {
@Upsert
suspend fun upsert(marmotKeyPackage: MarmotKeyPackage)
}

View File

@@ -0,0 +1,68 @@
package at.torch.compose.database.model
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey
import at.torch.compose.database.model.traits.BroadcastableEntity
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 com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlin.time.Clock
import kotlin.time.Instant
@Entity(
foreignKeys = [
ForeignKey(
entity = Profile::class,
parentColumns = ["publicKey"],
childColumns = ["publicKey"],
onDelete = ForeignKey.CASCADE,
),
]
)
data class MarmotKeyPackage(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val publicKey: HexKey,
val tlsEncodedMarmotKeyPackage: ByteArray,
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = Clock.System.now(),
override val savedAt: Instant = Clock.System.now(),
override val deletedAt: Instant? = null,
override val broadcastedAt: Instant? = null,
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as MarmotKeyPackage
if (id != other.id) return false
if (publicKey != other.publicKey) return false
if (!tlsEncodedMarmotKeyPackage.contentEquals(other.tlsEncodedMarmotKeyPackage)) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
if (savedAt != other.savedAt) return false
if (deletedAt != other.deletedAt) return false
if (broadcastedAt != other.broadcastedAt) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + publicKey.hashCode()
result = 31 * result + tlsEncodedMarmotKeyPackage.contentHashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()
result = 31 * result + savedAt.hashCode()
result = 31 * result + (deletedAt?.hashCode() ?: 0)
result = 31 * result + (broadcastedAt?.hashCode() ?: 0)
return result
}
}

View File

@@ -0,0 +1,42 @@
package at.torch.compose.database.model
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey
import at.torch.compose.database.model.traits.BroadcastableEntity
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 com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlin.time.Clock
import kotlin.time.Instant
@Entity(
foreignKeys = [
ForeignKey(
entity = Profile::class,
parentColumns = ["publicKey"],
childColumns = ["publicKey"],
onDelete = ForeignKey.CASCADE,
),
]
)
data class MarmotKeyPackageBundle(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val publicKey: HexKey,
val tlsEncodedMarmotKeyPackage: String,
val ncryptsecInitPrivateKey: String,
val ncryptsecEncryptionPrivateKey: String,
val ncryptsecSignaturePrivateKey: String,
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = Clock.System.now(),
override val savedAt: Instant = Clock.System.now(),
override val deletedAt: Instant? = null,
override val broadcastedAt: Instant? = null,
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity {
}

View File

@@ -3,6 +3,10 @@ package at.torch.compose.database.model
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey
import at.torch.compose.database.model.traits.BroadcastableEntity
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 com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlin.time.Clock
import kotlin.time.Instant
@@ -37,7 +41,7 @@ data class Mention(
override val savedAt: Instant = Clock.System.now(),
override val deletedAt: Instant? = null,
override val broadcastedAt: Instant? = null,
): at.torch.compose.database.model.traits.TimestampedEntity,
at.torch.compose.database.model.traits.LocalStoreEntity,
at.torch.compose.database.model.traits.BroadcastableEntity,
at.torch.compose.database.model.traits.SoftDeletableEntity
): TimestampedEntity,
LocalStoreEntity,
BroadcastableEntity,
SoftDeletableEntity

View File

@@ -0,0 +1,37 @@
package at.torch.compose.database.repository
import at.torch.compose.database.TorchDatabase
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.extensions.toHex
import at.torch.compose.repository.MarmotRepository
import com.vitorpamplona.quartz.marmot.mls.codec.TlsWriter
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
class DatabaseMarmotRepository(
val database: TorchDatabase
): MarmotRepository {
override suspend fun saveMarmotKeyPackageBundle(
publicKey: HexKey,
keyPackageBundle: KeyPackageBundle,
nsecPassword: String
) {
val tlsWriter = TlsWriter()
keyPackageBundle.keyPackage.encodeTls(tlsWriter)
val nip49 = Nip49()
database.marmotKeyPackageBundleDao().upsert(
MarmotKeyPackageBundle(
publicKey = publicKey,
tlsEncodedMarmotKeyPackage = tlsWriter.toByteArray().toHex(),
ncryptsecInitPrivateKey = nip49.encrypt(keyPackageBundle.initPrivateKey.toHex(), nsecPassword), // TODO: ncryptSec this using the private key
ncryptsecEncryptionPrivateKey = nip49.encrypt(keyPackageBundle.encryptionPrivateKey.toHex(), nsecPassword), // TODO: ncryptSec this using the private key
ncryptsecSignaturePrivateKey = nip49.encrypt(keyPackageBundle.signaturePrivateKey.toHex(), nsecPassword) // TODO: ncryptSec this using the private key
)
)
}
}

View File

@@ -1,9 +1,6 @@
package at.torch.compose.database.repository
import at.torch.compose.database.GENESIS_AT
import at.torch.compose.database.model.BroadcastNostrEventReceipt
import at.torch.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation
import at.torch.compose.database.model.RecentSearch
import at.torch.compose.database.model.MarmotKeyPackageBundle
import at.torch.compose.database.model.UnsignedNostrEvent
import at.torch.compose.nostr.Relays
import co.touchlab.kermit.Logger
@@ -11,6 +8,7 @@ import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.KEY_PACKAGE_LIFETIME_SECONDS
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
import com.vitorpamplona.quartz.marmot.mls.codec.TlsWriter
import com.vitorpamplona.quartz.marmot.mls.crypto.Ed25519
import com.vitorpamplona.quartz.marmot.mls.crypto.MlsCryptoProvider
import com.vitorpamplona.quartz.marmot.mls.crypto.X25519
@@ -48,14 +46,11 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -118,7 +113,7 @@ class DatabaseNostrRepository(
publicKey: HexKey,
name: String?,
biography: String?
) {
): KeyPackageBundle {
val profileEventTemplate = MetadataEvent.createNew(
name = name,
about = biography,
@@ -213,8 +208,6 @@ class DatabaseNostrRepository(
val identity = publicKey.hexToByteArray()
val bundle = generateKeyPackage(identity, dTag)
// TODO: Save the bundle?
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
val keyPackageBase64 = Base64.encode(keyPackageBytes)
val keyPackageRef = bundle.keyPackage.reference().toHexKey()
@@ -236,6 +229,8 @@ class DatabaseNostrRepository(
)
saveUnsignedNostrEvents(unsignedNostrEvents)
return bundle
}
private fun generateKeyPackage(

View File

@@ -0,0 +1,26 @@
package at.torch.compose.repository
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
import com.vitorpamplona.quartz.nip01Core.core.HexKey
interface MarmotRepository {
suspend fun saveMarmotKeyPackageBundle(
publicKey: HexKey,
keyPackageBundle: KeyPackageBundle,
nsecPassword: String
)
companion object {
private val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
override suspend fun saveMarmotKeyPackageBundle(
publicKey: HexKey,
keyPackageBundle: KeyPackageBundle,
nsecPassword: String
) {
TODO("Not yet implemented")
}
}
}
}

View File

@@ -1,5 +1,6 @@
package at.torch.compose.repository
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -24,7 +25,7 @@ interface NostrRepository {
publicKey: HexKey,
name: String?,
biography: String?
)
): KeyPackageBundle
suspend fun signInToProfile(
publicKey: HexKey
@@ -155,7 +156,8 @@ interface NostrRepository {
publicKey: HexKey,
name: String?,
biography: String?
) {
): KeyPackageBundle {
TODO("")
}
override suspend fun signInToProfile(publicKey: HexKey) {

View File

@@ -90,6 +90,7 @@ class ChatRoomCreationViewModel(
viewModelScope.launch(Dispatchers.Main) {
if (localChatRoom != null) {
// TODO: Broadcast the thing...
// TODO: Navigate to invitation screen...
onNavigateToChatRoom.invoke(
ChatRoomDetailRoute(

View File

@@ -10,6 +10,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.ui.view.state.form.CreateProfileFormState
import androidx.lifecycle.viewModelScope
import at.torch.compose.repository.MarmotRepository
import co.touchlab.kermit.Logger
import fr.acinq.bitcoin.Chain
import fr.acinq.bitcoin.MnemonicCode
@@ -18,6 +19,7 @@ import fr.acinq.lightning.Lightning
import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.managers.nostrPublicKey
import fr.acinq.phoenix.managers.nsecPassword
import fr.acinq.phoenix.utils.MnemonicLanguage
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
@@ -27,19 +29,22 @@ import kotlinx.coroutines.launch
class CreateProfileViewModel(
val initialCreateProfileUIState: CreateProfileUIState,
val createProfileFormState: CreateProfileFormState = CreateProfileFormState(),
val nostrRepository: NostrRepository
val nostrRepository: NostrRepository,
val marmotRepository: MarmotRepository
): ViewModel() {
companion object {
private const val TAG = "CreateAccountViewModel"
fun factory(
initialCreateProfileUIState: CreateProfileUIState,
nostrRepository: NostrRepository
nostrRepository: NostrRepository,
marmotRepository: MarmotRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
CreateProfileViewModel(
initialCreateProfileUIState,
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
marmotRepository = marmotRepository
)
}
}
@@ -100,12 +105,17 @@ class CreateProfileViewModel(
)
val pubkey = localKeyManager.nostrPublicKey()
logger.d("NostrKey: $pubkey")
nostrRepository.createNewProfile(
val keyPackageBundle = nostrRepository.createNewProfile(
pubkey,
name = createProfileFormState.nameField.textFieldState.text.toString(),
biography = createProfileFormState.biographyField.textFieldState.text.toString()
)
marmotRepository.saveMarmotKeyPackageBundle(
publicKey = pubkey,
keyPackageBundle,
nsecPassword = localKeyManager.nsecPassword()
)
}
}

View File

@@ -102,6 +102,12 @@ fun LocalKeyManager.nostrPrivateKey(): PrivateKey {
return derivePrivateKey(path).privateKey
}
fun LocalKeyManager.nsecPassword(): String {
return Crypto.hash160(
nostrPrivateKey().value
).byteVector().toHex()
}
fun LocalKeyManager.nostrPublicKey(): HexKey {
return KeyPair(
privKey = nostrPrivateKey().value.toByteArray()