Generates keypackages with 64 chars

This commit is contained in:
Vitor Pamplona
2026-04-20 12:00:31 -04:00
parent f71d8eebb8
commit 4f2f8e180e
4 changed files with 73 additions and 32 deletions
@@ -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>()
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 numEventIds = reader.readUint32().toInt()
repeat(numEventIds) {
val eventId = reader.readOpaque2().decodeToString()
val slot = reader.readOpaque2().decodeToString()
eventIdMap[eventId] = slot
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
}
}
@@ -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.
*