Skip to content
Open
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ refinery = "0.9"

# Random number generation
getrandom = "0.4"
x25519-dalek = { version = "2.0", default-features = false, features = ["static_secrets", "zeroize"] }

# Secure credential storage (v4 architecture - see https://github.com/open-source-cooperative/keyring-rs/issues/259)
keyring-core = "0.7"
Expand Down
9 changes: 9 additions & 0 deletions crates/mdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@

### Added

- Added feature-gated `mip06` module implementing MIP-06 multi-device support. A single Nostr identity can now participate in MLS groups from multiple independent devices, each as its own MLS leaf node. The pairing flow uses X25519 key agreement with ChaCha20-Poly1305 authenticated encryption for a two-phase encrypted exchange (KeyPackage out, Welcome back) over an out-of-band channel. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `create_group_with_multi_device` for creating groups with the `MarmotMultiDevice` group context extension pre-enabled, and `enable_multi_device` for enabling MIP-06 on existing groups via a GroupContextExtensions proposal. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `add_device_to_groups` and `build_pairing_payload` for existing-device-side pairing: generates Welcome messages (Add-based flow) or GroupInfo + join PSK bundles (External Commit flow) for the new device. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `join_group_via_external_commit` for new-device-side pairing: constructs an External Commit with a Nostr identity proof in authenticated_data and a join PSK proposal for group admission. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `own_device_leaves` to return all leaf indices in a group belonging to the local user's Nostr pubkey, enabling device-aware UI rendering. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `register_join_psk` for pre-registering MIP-06 join PSKs derived from MLS exporter secrets, enabling External Commit acceptance by existing group members. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added External Commit validation in the message processing pipeline with identity proof verification binding the joining device's credential to an existing member's Nostr pubkey. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added TLS-serializable pairing payload types: `GroupPairingDataV1`, `PairingPayload`, `DevicePairingRequest`, `DevicePairingResponse`, and `GroupWelcomeData` with version validation. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `EncryptedDeviceName` for NIP-44 encrypted device name labels, readable only by the owning Nostr identity. ([#251](https://github.com/marmot-protocol/mdk/pull/251))
- Added `delete_messages_for_group` and `delete_group` public methods on `MDK` for local "clear chat" and "delete chat" operations. `delete_group` also cleans up `EpochSnapshotManager` in-memory state. Neither operation publishes MLS proposals or Nostr events. ([#250](https://github.com/marmot-protocol/mdk/pull/250))
- Added ThumbHash preview generation alongside existing BlurHash support. `MediaProcessingOptions` now includes `generate_thumbhash`, `ImageMetadata`, `MediaMetadata`, `EncryptedMediaUpload`, and `GroupImageUpload` expose optional `thumbhash`, and encrypted-media IMETA writing emits `thumbhash` while parsing accepts both `blurhash` and `thumbhash` tags for compatibility. ([#244](https://github.com/marmot-protocol/mdk/pull/244))
- SelfRemove proposal type (`0x000a`) added to client capabilities, group required capabilities, and KeyPackage `mls_proposals` tag per MIP-03.
Expand Down
3 changes: 3 additions & 0 deletions crates/mdk-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ categories = ["cryptography", "network-programming"]
default = []
mip04 = []
mip05 = ["nostr/nip59"]
mip06 = ["dep:x25519-dalek", "dep:getrandom"]
debug-examples = []

[dependencies]
Expand All @@ -32,6 +33,8 @@ tls_codec.workspace = true
tracing = { workspace = true, features = ["std"] }
thiserror.workspace = true
zeroize.workspace = true
x25519-dalek = { workspace = true, optional = true }
getrandom = { workspace = true, optional = true }

# Crypto dependencies for MIP-01 (group image encryption) and MIP-03 message encryption - mandatory
base64.workspace = true
Expand Down
22 changes: 22 additions & 0 deletions crates/mdk-core/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ pub const MLS_KEY_PACKAGE_KIND_LEGACY: Kind = Kind::Custom(443);
/// Nostr Group Data extension type
pub const NOSTR_GROUP_DATA_EXTENSION_TYPE: u16 = 0xF2EE; // Be FREE

/// MIP-06: Multi-Device extension type
#[cfg(feature = "mip06")]
pub const MULTI_DEVICE_EXTENSION_TYPE: u16 = 0xF2F0;

/// MIP-06: Device Name extension type (optional per-leaf metadata)
#[cfg(feature = "mip06")]
pub const DEVICE_NAME_EXTENSION_TYPE: u16 = 0xF2EF;

/// Default ciphersuite for Nostr Groups.
/// This is also the only required ciphersuite for Nostr Groups.
pub const DEFAULT_CIPHERSUITE: Ciphersuite =
Expand All @@ -36,11 +44,21 @@ pub const DEFAULT_CIPHERSUITE: Ciphersuite =
/// Note: LastResort (0x000a) is included here because OpenMLS requires KeyPackage-level
/// extensions to be declared in capabilities for validation, even though per the MLS
/// Extensions draft it's technically just a KeyPackage marker.
#[cfg(not(feature = "mip06"))]
pub const SUPPORTED_EXTENSIONS: [ExtensionType; 2] = [
ExtensionType::LastResort, // 0x000A - Required by OpenMLS validation
ExtensionType::Unknown(NOSTR_GROUP_DATA_EXTENSION_TYPE), // 0xF2EE - NostrGroupData
];

/// When MIP-06 is enabled, additionally advertise multi-device and device-name support.
#[cfg(feature = "mip06")]
pub const SUPPORTED_EXTENSIONS: [ExtensionType; 4] = [
ExtensionType::LastResort, // 0x000A
ExtensionType::Unknown(NOSTR_GROUP_DATA_EXTENSION_TYPE), // 0xF2EE
ExtensionType::Unknown(DEVICE_NAME_EXTENSION_TYPE), // 0xF2EF
ExtensionType::Unknown(MULTI_DEVICE_EXTENSION_TYPE), // 0xF2F0
];

/// Extensions that are required in the GroupContext RequiredCapabilities extension.
///
/// This enforces that all group members must support these extensions. For Marmot,
Expand All @@ -55,8 +73,12 @@ pub const GROUP_CONTEXT_REQUIRED_EXTENSIONS: [ExtensionType; 1] = [
/// Derived from SUPPORTED_EXTENSIONS to guarantee the tags accurately advertise
/// what the KeyPackage capabilities contain. GREASE values are excluded — they
/// are injected dynamically at runtime (see `MDK::capabilities()`).
#[cfg(not(feature = "mip06"))]
pub const TAG_EXTENSIONS: [ExtensionType; 2] = SUPPORTED_EXTENSIONS;

#[cfg(feature = "mip06")]
pub const TAG_EXTENSIONS: [ExtensionType; 4] = SUPPORTED_EXTENSIONS;

/// Non-default proposal types that clients advertise support for.
///
/// Per the MLS Extensions draft, SelfRemove (0x000a) is not a default
Expand Down
15 changes: 15 additions & 0 deletions crates/mdk-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@ pub enum Error {
/// Failed to create epoch snapshot for commit race resolution
#[error("failed to create epoch snapshot: {0}")]
SnapshotCreationFailed(String),
/// MIP-06: Multi-device signaling gate not satisfied
#[cfg(feature = "mip06")]
#[cfg_attr(docsrs, doc(cfg(feature = "mip06")))]
#[error("multi-device not enabled for this group")]
MultiDeviceNotEnabled,
/// MIP-06: Device pairing error
#[cfg(feature = "mip06")]
#[cfg_attr(docsrs, doc(cfg(feature = "mip06")))]
#[error("pairing error: {0}")]
PairingError(String),
/// MIP-06: Nostr identity proof error
#[cfg(feature = "mip06")]
#[cfg_attr(docsrs, doc(cfg(feature = "mip06")))]
#[error("identity proof error: {0}")]
IdentityProofError(String),
}

impl From<FromUtf8Error> for Error {
Expand Down
Loading
Loading