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.hash
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
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.marmot.mls.group.MlsGroupStateStore
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
@@ -2211,23 +2210,13 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a KeyPackage has been published, either locally generated
|
* Check if a KeyPackage has been published in this session.
|
||||||
* in this session or found in the local cache from a previous 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 {
|
suspend fun hasPublishedKeyPackage(): Boolean {
|
||||||
// Check in-memory bundles first (current session)
|
val manager = marmotManager ?: return false
|
||||||
val manager = marmotManager
|
return manager.hasActiveKeyPackages()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+5
-4
@@ -296,10 +296,11 @@ class MarmotManager(
|
|||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
suspend fun generateKeyPackageEvent(
|
suspend fun generateKeyPackageEvent(
|
||||||
relays: List<NormalizedRelayUrl>,
|
relays: List<NormalizedRelayUrl>,
|
||||||
dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT,
|
slotName: String = KeyPackageUtils.PRIMARY_SLOT,
|
||||||
): KeyPackageEvent {
|
): KeyPackageEvent {
|
||||||
|
val dTag = keyPackageRotationManager.getOrCreateSlotDTag(slotName)
|
||||||
val identity = signer.pubKey.hexToByteArray()
|
val identity = signer.pubKey.hexToByteArray()
|
||||||
val bundle = keyPackageRotationManager.generateKeyPackage(identity, dTagSlot)
|
val bundle = keyPackageRotationManager.generateKeyPackage(identity, dTag)
|
||||||
|
|
||||||
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
|
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
|
||||||
val keyPackageBase64 = Base64.encode(keyPackageBytes)
|
val keyPackageBase64 = Base64.encode(keyPackageBytes)
|
||||||
@@ -308,7 +309,7 @@ class MarmotManager(
|
|||||||
val template =
|
val template =
|
||||||
KeyPackageEvent.build(
|
KeyPackageEvent.build(
|
||||||
keyPackageBase64 = keyPackageBase64,
|
keyPackageBase64 = keyPackageBase64,
|
||||||
dTagSlot = dTagSlot,
|
dTagSlot = dTag,
|
||||||
keyPackageRef = keyPackageRef,
|
keyPackageRef = keyPackageRef,
|
||||||
relays = relays,
|
relays = relays,
|
||||||
)
|
)
|
||||||
@@ -317,7 +318,7 @@ class MarmotManager(
|
|||||||
// Welcome receivers identify the consumed KeyPackage by its Nostr
|
// Welcome receivers identify the consumed KeyPackage by its Nostr
|
||||||
// event id (the MIP-02 "e" tag), not by the MLS reference hash, so
|
// 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.
|
// remember the mapping right after we know the signed event id.
|
||||||
keyPackageRotationManager.recordPublishedEventId(dTagSlot, signed.id)
|
keyPackageRotationManager.recordPublishedEventId(dTag, signed.id)
|
||||||
return signed
|
return signed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-9
@@ -62,6 +62,13 @@ class KeyPackageRotationManager(
|
|||||||
private val activeBundles = mutableMapOf<String, KeyPackageBundle>()
|
private val activeBundles = mutableMapOf<String, KeyPackageBundle>()
|
||||||
private val pendingRotations = mutableSetOf<String>()
|
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
|
* 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
|
* d-tag slot whose bundle backs it. The welcome event references its
|
||||||
@@ -136,10 +143,13 @@ class KeyPackageRotationManager(
|
|||||||
pendingRotations.addAll(decoded.pending)
|
pendingRotations.addAll(decoded.pending)
|
||||||
eventIdToSlot.clear()
|
eventIdToSlot.clear()
|
||||||
eventIdToSlot.putAll(decoded.eventIdToSlot)
|
eventIdToSlot.putAll(decoded.eventIdToSlot)
|
||||||
|
namedSlotDTags.clear()
|
||||||
|
namedSlotDTags.putAll(decoded.namedSlotDTags)
|
||||||
}
|
}
|
||||||
Log.d("KeyPackageRotationManager") {
|
Log.d("KeyPackageRotationManager") {
|
||||||
"Restored ${decoded.bundles.size} active KeyPackage bundle(s), " +
|
"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) {
|
} catch (e: Exception) {
|
||||||
Log.w("KeyPackageRotationManager", "Failed to decode persisted KeyPackages: ${e.message}")
|
Log.w("KeyPackageRotationManager", "Failed to decode persisted KeyPackages: ${e.message}")
|
||||||
@@ -150,6 +160,7 @@ class KeyPackageRotationManager(
|
|||||||
val bundles: Map<String, KeyPackageBundle>,
|
val bundles: Map<String, KeyPackageBundle>,
|
||||||
val pending: Set<String>,
|
val pending: Set<String>,
|
||||||
val eventIdToSlot: Map<String, String>,
|
val eventIdToSlot: Map<String, String>,
|
||||||
|
val namedSlotDTags: Map<String, String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,6 +191,12 @@ class KeyPackageRotationManager(
|
|||||||
writer.putOpaque2(eventId.encodeToByteArray())
|
writer.putOpaque2(eventId.encodeToByteArray())
|
||||||
writer.putOpaque2(slot.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()
|
return writer.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,15 +230,22 @@ class KeyPackageRotationManager(
|
|||||||
pending.add(reader.readOpaque2().decodeToString())
|
pending.add(reader.readOpaque2().decodeToString())
|
||||||
}
|
}
|
||||||
val eventIdMap = mutableMapOf<String, String>()
|
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) {
|
if (reader.hasRemaining) {
|
||||||
val numEventIds = reader.readUint32().toInt()
|
val numNamed = reader.readUint32().toInt()
|
||||||
repeat(numEventIds) {
|
repeat(numNamed) {
|
||||||
val eventId = reader.readOpaque2().decodeToString()
|
val name = reader.readOpaque2().decodeToString()
|
||||||
val slot = reader.readOpaque2().decodeToString()
|
val dTag = reader.readOpaque2().decodeToString()
|
||||||
eventIdMap[eventId] = slot
|
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.
|
* Generate a new KeyPackage and its associated private bundle.
|
||||||
*
|
*
|
||||||
* @param identity the user's identity bytes (typically 32-byte Nostr pubkey)
|
* @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
|
* @return a [KeyPackageBundle] containing the KeyPackage and all private keys
|
||||||
*/
|
*/
|
||||||
suspend fun generateKeyPackage(
|
suspend fun generateKeyPackage(
|
||||||
@@ -480,7 +518,8 @@ class KeyPackageRotationManager(
|
|||||||
* On-disk snapshot format version for [KeyPackageBundleStore].
|
* On-disk snapshot format version for [KeyPackageBundleStore].
|
||||||
* v1: bundles + pendingRotations
|
* v1: bundles + pendingRotations
|
||||||
* v2: + eventIdToSlot map (so welcome lookup by Nostr event id works)
|
* 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
|
package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.tags.EncodingTag
|
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.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
|
|
||||||
@@ -41,12 +43,22 @@ object KeyPackageUtils {
|
|||||||
/** Current addressable KeyPackage kind */
|
/** Current addressable KeyPackage kind */
|
||||||
const val CURRENT_KIND = KeyPackageEvent.KIND // 30443
|
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 */
|
/** Maximum number of d-tag slots a user should maintain */
|
||||||
const val MAX_SLOTS = 10
|
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.
|
* Selects the best KeyPackage from a set of candidates for a given user.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user