From 4f2f8e180e5fe7ff076c62adabba074542982d75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 20 Apr 2026 12:00:31 -0400 Subject: [PATCH] Generates keypackages with 64 chars --- .../vitorpamplona/amethyst/model/Account.kt | 21 ++----- .../amethyst/commons/marmot/MarmotManager.kt | 9 +-- .../KeyPackageRotationManager.kt | 57 ++++++++++++++++--- .../mip00KeyPackages/KeyPackageUtils.kt | 18 +++++- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 79a6d4a8a..9dbe3c778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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() } /** diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt index d56abe2c0..a42d78e72 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt @@ -296,10 +296,11 @@ class MarmotManager( @OptIn(ExperimentalEncodingApi::class) suspend fun generateKeyPackageEvent( relays: List, - 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 } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRotationManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRotationManager.kt index c10e03901..b8c1e03cf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRotationManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRotationManager.kt @@ -62,6 +62,13 @@ class KeyPackageRotationManager( private val activeBundles = mutableMapOf() private val pendingRotations = mutableSetOf() + /** + * 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() + /** * 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, val pending: Set, val eventIdToSlot: Map, + val namedSlotDTags: Map, ) /** @@ -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() + val numEventIds = reader.readUint32().toInt() + repeat(numEventIds) { + val eventId = reader.readOpaque2().decodeToString() + val slot = reader.readOpaque2().decodeToString() + eventIdMap[eventId] = slot + } + val namedSlots = mutableMapOf() 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 } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageUtils.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageUtils.kt index c4522057c..8d9e7a790 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageUtils.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageUtils.kt @@ -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. *