Generates keypackages with 64 chars
This commit is contained in:
@@ -136,7 +136,6 @@ import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.hash
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
@@ -2211,23 +2210,13 @@ class Account(
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a KeyPackage has been published, either locally generated
|
||||
* in this session or found in the local cache from a previous session.
|
||||
* Check if a KeyPackage has been published in this session.
|
||||
* The d-tag is a randomly-generated value stored in the KeyPackageRotationManager's
|
||||
* persisted snapshot, so there is no fixed address to query in the cache.
|
||||
*/
|
||||
suspend fun hasPublishedKeyPackage(): Boolean {
|
||||
// Check in-memory bundles first (current session)
|
||||
val manager = marmotManager
|
||||
if (manager != null && manager.hasActiveKeyPackages()) return true
|
||||
|
||||
// Check local cache for our own kind:30443 events (from previous sessions / relay downloads)
|
||||
val address =
|
||||
com.vitorpamplona.quartz.nip01Core.core.Address(
|
||||
KeyPackageEvent.KIND,
|
||||
signer.pubKey,
|
||||
KeyPackageUtils.PRIMARY_SLOT,
|
||||
)
|
||||
val note = cache.getAddressableNoteIfExists(address)
|
||||
return note?.event != null
|
||||
val manager = marmotManager ?: return false
|
||||
return manager.hasActiveKeyPackages()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+5
-4
@@ -296,10 +296,11 @@ class MarmotManager(
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
suspend fun generateKeyPackageEvent(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT,
|
||||
slotName: String = KeyPackageUtils.PRIMARY_SLOT,
|
||||
): KeyPackageEvent {
|
||||
val dTag = keyPackageRotationManager.getOrCreateSlotDTag(slotName)
|
||||
val identity = signer.pubKey.hexToByteArray()
|
||||
val bundle = keyPackageRotationManager.generateKeyPackage(identity, dTagSlot)
|
||||
val bundle = keyPackageRotationManager.generateKeyPackage(identity, dTag)
|
||||
|
||||
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
|
||||
val keyPackageBase64 = Base64.encode(keyPackageBytes)
|
||||
@@ -308,7 +309,7 @@ class MarmotManager(
|
||||
val template =
|
||||
KeyPackageEvent.build(
|
||||
keyPackageBase64 = keyPackageBase64,
|
||||
dTagSlot = dTagSlot,
|
||||
dTagSlot = dTag,
|
||||
keyPackageRef = keyPackageRef,
|
||||
relays = relays,
|
||||
)
|
||||
@@ -317,7 +318,7 @@ class MarmotManager(
|
||||
// Welcome receivers identify the consumed KeyPackage by its Nostr
|
||||
// event id (the MIP-02 "e" tag), not by the MLS reference hash, so
|
||||
// remember the mapping right after we know the signed event id.
|
||||
keyPackageRotationManager.recordPublishedEventId(dTagSlot, signed.id)
|
||||
keyPackageRotationManager.recordPublishedEventId(dTag, signed.id)
|
||||
return signed
|
||||
}
|
||||
|
||||
|
||||
+44
-5
@@ -62,6 +62,13 @@ class KeyPackageRotationManager(
|
||||
private val activeBundles = mutableMapOf<String, KeyPackageBundle>()
|
||||
private val pendingRotations = mutableSetOf<String>()
|
||||
|
||||
/**
|
||||
* Maps logical slot names (e.g., "primary") to their persisted random d-tag values.
|
||||
* Per MIP-00, each slot gets a cryptographically random 64-char hex d-tag on first
|
||||
* use, which is then reused for all rotations of that slot.
|
||||
*/
|
||||
private val namedSlotDTags = mutableMapOf<String, String>()
|
||||
|
||||
/**
|
||||
* Maps the Nostr event id (kind:30443) of a published KeyPackage to the
|
||||
* d-tag slot whose bundle backs it. The welcome event references its
|
||||
@@ -136,10 +143,13 @@ class KeyPackageRotationManager(
|
||||
pendingRotations.addAll(decoded.pending)
|
||||
eventIdToSlot.clear()
|
||||
eventIdToSlot.putAll(decoded.eventIdToSlot)
|
||||
namedSlotDTags.clear()
|
||||
namedSlotDTags.putAll(decoded.namedSlotDTags)
|
||||
}
|
||||
Log.d("KeyPackageRotationManager") {
|
||||
"Restored ${decoded.bundles.size} active KeyPackage bundle(s), " +
|
||||
"${decoded.pending.size} pending rotation, ${decoded.eventIdToSlot.size} eventId mapping(s)"
|
||||
"${decoded.pending.size} pending rotation, ${decoded.eventIdToSlot.size} eventId mapping(s), " +
|
||||
"${decoded.namedSlotDTags.size} named slot d-tag(s)"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("KeyPackageRotationManager", "Failed to decode persisted KeyPackages: ${e.message}")
|
||||
@@ -150,6 +160,7 @@ class KeyPackageRotationManager(
|
||||
val bundles: Map<String, KeyPackageBundle>,
|
||||
val pending: Set<String>,
|
||||
val eventIdToSlot: Map<String, String>,
|
||||
val namedSlotDTags: Map<String, String>,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -180,6 +191,12 @@ class KeyPackageRotationManager(
|
||||
writer.putOpaque2(eventId.encodeToByteArray())
|
||||
writer.putOpaque2(slot.encodeToByteArray())
|
||||
}
|
||||
// named slot → actual random d-tag (added in v3)
|
||||
writer.putUint32(namedSlotDTags.size.toLong())
|
||||
for ((name, dTag) in namedSlotDTags) {
|
||||
writer.putOpaque2(name.encodeToByteArray())
|
||||
writer.putOpaque2(dTag.encodeToByteArray())
|
||||
}
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
@@ -213,15 +230,22 @@ class KeyPackageRotationManager(
|
||||
pending.add(reader.readOpaque2().decodeToString())
|
||||
}
|
||||
val eventIdMap = mutableMapOf<String, String>()
|
||||
if (reader.hasRemaining) {
|
||||
val numEventIds = reader.readUint32().toInt()
|
||||
repeat(numEventIds) {
|
||||
val eventId = reader.readOpaque2().decodeToString()
|
||||
val slot = reader.readOpaque2().decodeToString()
|
||||
eventIdMap[eventId] = slot
|
||||
}
|
||||
val namedSlots = mutableMapOf<String, String>()
|
||||
if (reader.hasRemaining) {
|
||||
val numNamed = reader.readUint32().toInt()
|
||||
repeat(numNamed) {
|
||||
val name = reader.readOpaque2().decodeToString()
|
||||
val dTag = reader.readOpaque2().decodeToString()
|
||||
namedSlots[name] = dTag
|
||||
}
|
||||
return Snapshot(bundles, pending, eventIdMap)
|
||||
}
|
||||
return Snapshot(bundles, pending, eventIdMap, namedSlots)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,11 +262,25 @@ class KeyPackageRotationManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual d-tag (64-char random hex) for a logical slot name, generating
|
||||
* and persisting a fresh random value on first call for that name.
|
||||
*
|
||||
* Per MIP-00: "Clients SHOULD generate a cryptographically random 32-byte hex string
|
||||
* as the d tag value when first publishing a KeyPackage."
|
||||
*/
|
||||
suspend fun getOrCreateSlotDTag(logicalSlotName: String): String =
|
||||
mutex.withLock {
|
||||
namedSlotDTags.getOrPut(logicalSlotName) {
|
||||
KeyPackageUtils.generateRandomDTag().also { persistUnlocked() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new KeyPackage and its associated private bundle.
|
||||
*
|
||||
* @param identity the user's identity bytes (typically 32-byte Nostr pubkey)
|
||||
* @param dTagSlot the d-tag slot for addressable replacement
|
||||
* @param dTagSlot the actual d-tag value (64-char hex) for addressable replacement
|
||||
* @return a [KeyPackageBundle] containing the KeyPackage and all private keys
|
||||
*/
|
||||
suspend fun generateKeyPackage(
|
||||
@@ -480,7 +518,8 @@ class KeyPackageRotationManager(
|
||||
* On-disk snapshot format version for [KeyPackageBundleStore].
|
||||
* v1: bundles + pendingRotations
|
||||
* v2: + eventIdToSlot map (so welcome lookup by Nostr event id works)
|
||||
* v3: + namedSlotDTags map (per MIP-00, d-tags are random 64-char hex, persisted here)
|
||||
*/
|
||||
private const val SNAPSHOT_VERSION = 2
|
||||
private const val SNAPSHOT_VERSION = 3
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -21,8 +21,10 @@
|
||||
package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.tags.EncodingTag
|
||||
import com.vitorpamplona.quartz.marmot.mls.crypto.MlsCryptoProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
|
||||
@@ -41,12 +43,22 @@ object KeyPackageUtils {
|
||||
/** Current addressable KeyPackage kind */
|
||||
const val CURRENT_KIND = KeyPackageEvent.KIND // 30443
|
||||
|
||||
/** Default d-tag slot for primary KeyPackage */
|
||||
const val PRIMARY_SLOT = "0"
|
||||
|
||||
/** Maximum number of d-tag slots a user should maintain */
|
||||
const val MAX_SLOTS = 10
|
||||
|
||||
/**
|
||||
* Logical name for the primary KeyPackage slot.
|
||||
* This is NOT the actual d-tag value — the actual d-tag is a randomly
|
||||
* generated 64-char hex string stored persistently in [KeyPackageRotationManager].
|
||||
*/
|
||||
const val PRIMARY_SLOT = "primary"
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random d-tag value per MIP-00:
|
||||
* a 32-byte (64 hex char) random identifier for a KeyPackage slot.
|
||||
*/
|
||||
fun generateRandomDTag(): String = MlsCryptoProvider.randomBytes(32).toHexKey()
|
||||
|
||||
/**
|
||||
* Selects the best KeyPackage from a set of candidates for a given user.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user