use std::string::FromUtf8Error;
use std::{fmt, str};
use nostr::types::url;
use nostr::{Kind, PublicKey, SignerError, event, key};
use openmls::credentials::errors::BasicCredentialError;
use openmls::error::LibraryError;
use openmls::extensions::errors::InvalidExtensionError;
use openmls::framing::errors::ProtocolMessageError;
use openmls::group::{
AddMembersError, CommitToPendingProposalsError, CreateGroupContextExtProposalError,
CreateMessageError, ExportSecretError, MergePendingCommitError, NewGroupError,
ProcessMessageError, SelfUpdateError, WelcomeError,
};
use openmls::key_packages::errors::{KeyPackageNewError, KeyPackageVerifyError};
use openmls::prelude::{MlsGroupStateError, ProposalType, ValidationError};
use openmls_traits::types::CryptoError;
#[cfg(feature = "mip05")]
use crate::mip05::Mip05Error;
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Hex(#[from] hex::FromHexError),
#[error(transparent)]
Keys(#[from] key::Error),
#[error(transparent)]
Event(#[from] event::Error),
#[error(transparent)]
EventBuilder(#[from] event::builder::Error),
#[error(transparent)]
Signer(#[from] SignerError),
#[error(transparent)]
RelayUrl(#[from] url::Error),
#[error(transparent)]
Tls(#[from] tls_codec::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
Storage(#[from] mdk_storage_traits::MdkStorageError),
#[error(transparent)]
OpenMlsGeneric(#[from] LibraryError),
#[error(transparent)]
InvalidExtension(#[from] InvalidExtensionError),
#[error(transparent)]
CreateMessage(#[from] CreateMessageError),
#[error(transparent)]
ExportSecret(#[from] ExportSecretError),
#[error(transparent)]
BasicCredential(#[from] BasicCredentialError),
#[error("Message epoch differs from the group's epoch")]
ProcessMessageWrongEpoch(u64, bool),
#[error("Wrong group ID")]
ProcessMessageWrongGroupId,
#[error("Use after eviction")]
ProcessMessageUseAfterEviction,
#[error("{0}")]
ProcessMessageOther(String),
#[error("{0}")]
ProtocolMessage(String),
#[error("{0}")]
KeyPackage(String),
#[error("{0}")]
Group(String),
#[error("invitee KeyPackage is missing a proposal type required by the group")]
InviteeMissingRequiredProposal,
#[error("only admins can perform this operation")]
NotAdmin,
#[error("upgrade requires at least one proposal type")]
EmptyUpgradeSet,
#[error("proposal type {0:?} is not in SUPPORTED_PROPOSALS")]
ProposalNotInSupportedSet(ProposalType),
#[error("proposal type {0:?} is already required by this group")]
ProposalAlreadyRequired(ProposalType),
#[error(
"proposal type {proposal:?} can't be upgraded: {} member(s) don't advertise it",
blockers.len()
)]
ProposalNotAvailableForUpgrade {
proposal: ProposalType,
blockers: Vec<PublicKey>,
},
#[error("group exporter secret not found")]
GroupExporterSecretNotFound,
#[error("{0}")]
Message(String),
#[cfg(feature = "mip05")]
#[cfg_attr(docsrs, doc(cfg(feature = "mip05")))]
#[error(transparent)]
Mip05(#[from] Mip05Error),
#[error("cannot decrypt own message")]
CannotDecryptOwnMessage,
#[error("{0}")]
MergePendingCommit(String),
#[error("unable to commit to pending proposal")]
CommitToPendingProposalsError,
#[error("{0}")]
SelfUpdate(String),
#[error("{0}")]
Welcome(String),
#[error("welcome previously failed to process: {0}")]
WelcomePreviouslyFailed(String),
#[error("processed welcome not found")]
ProcessedWelcomeNotFound,
#[error("{0}")]
Provider(String),
#[error("group not found")]
GroupNotFound,
#[error("protocol message group ID doesn't match the current group ID")]
ProtocolGroupIdMismatch,
#[error("own leaf not found")]
OwnLeafNotFound,
#[error("can't load signer")]
CantLoadSigner,
#[error("invalid welcome message")]
InvalidWelcomeMessage,
#[error("unexpected event kind: expected={expected}, received={received}")]
UnexpectedEvent {
expected: Kind,
received: Kind,
},
#[error("Unexpected extension type")]
UnexpectedExtensionType,
#[error("Nostr group data extension not found")]
NostrGroupDataExtensionNotFound,
#[error("Message received from non-member")]
MessageFromNonMember,
#[error("{0}")]
NotImplemented(String),
#[error("stored message not found")]
MessageNotFound,
#[error("not processing commit from non-admin")]
CommitFromNonAdmin,
#[error("own commit pending merge")]
OwnCommitPending,
#[error("Error when updating group context extensions {0}")]
UpdateGroupContextExts(String),
#[error("invalid image hash length")]
InvalidImageHashLength,
#[error("invalid image key length")]
InvalidImageKeyLength,
#[error("invalid image nonce length")]
InvalidImageNonceLength,
#[error("invalid image upload key length")]
InvalidImageUploadKeyLength,
#[error("invalid extension version: {0}")]
InvalidExtensionVersion(u16),
#[error("extension format error: {0}")]
ExtensionFormatError(String),
#[error("author mismatch: rumor pubkey does not match MLS sender")]
AuthorMismatch,
#[error(
"key package identity mismatch: credential identity {credential_identity} doesn't match event signer {event_signer}"
)]
KeyPackageIdentityMismatch {
credential_identity: String,
event_signer: String,
},
#[error(
"identity change not allowed: proposal attempts to change identity from {original_identity} to {new_identity}"
)]
IdentityChangeNotAllowed {
original_identity: String,
new_identity: String,
},
#[error("rumor event is missing its ID")]
MissingRumorEventId,
#[error("event timestamp is invalid: {0}")]
InvalidTimestamp(String),
#[error("missing required group ID tag (h tag)")]
MissingGroupIdTag,
#[error("invalid group ID format: {0}")]
InvalidGroupIdFormat(String),
#[error("multiple group ID tags found: expected exactly one h tag, found {0}")]
MultipleGroupIdTags(usize),
#[error("failed to create epoch snapshot: {0}")]
SnapshotCreationFailed(String),
}
impl From<FromUtf8Error> for Error {
fn from(e: FromUtf8Error) -> Self {
Self::Utf8(e.utf8_error())
}
}
macro_rules! impl_from_display_error {
($($source:ty => $variant:ident),+ $(,)?) => {
$(
impl From<$source> for Error {
fn from(e: $source) -> Self {
Self::$variant(e.to_string())
}
}
)+
};
}
macro_rules! impl_from_generic_display_error {
($($source:ident => $variant:ident),+ $(,)?) => {
$(
impl<T: fmt::Display> From<$source<T>> for Error {
fn from(e: $source<T>) -> Self {
Self::$variant(e.to_string())
}
}
)+
};
}
impl_from_display_error! {
ProtocolMessageError => ProtocolMessage,
KeyPackageNewError => KeyPackage,
KeyPackageVerifyError => KeyPackage,
}
impl_from_generic_display_error! {
NewGroupError => Group,
AddMembersError => Group,
MergePendingCommitError => MergePendingCommit,
SelfUpdateError => SelfUpdate,
WelcomeError => Welcome,
CreateGroupContextExtProposalError => UpdateGroupContextExts,
}
impl<T: fmt::Display> From<CommitToPendingProposalsError<T>> for Error {
fn from(_e: CommitToPendingProposalsError<T>) -> Self {
Self::CommitToPendingProposalsError
}
}
impl<T> From<ProcessMessageError<T>> for Error
where
T: fmt::Display,
{
fn from(e: ProcessMessageError<T>) -> Self {
match e {
ProcessMessageError::ValidationError(validation_error) => match validation_error {
ValidationError::WrongGroupId => Self::ProcessMessageWrongGroupId,
ValidationError::CannotDecryptOwnMessage => Self::CannotDecryptOwnMessage,
_ => Self::ProcessMessageOther(validation_error.to_string()),
},
ProcessMessageError::GroupStateError(group_state_error) => match group_state_error {
MlsGroupStateError::UseAfterEviction => Self::ProcessMessageUseAfterEviction,
_ => Self::ProcessMessageOther(group_state_error.to_string()),
},
_ => Self::ProcessMessageOther(e.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nostr::Kind;
#[test]
fn test_error_display_messages() {
let error = Error::ProcessMessageWrongEpoch(5, true);
assert_eq!(
error.to_string(),
"Message epoch differs from the group's epoch"
);
let error = Error::ProcessMessageWrongGroupId;
assert_eq!(error.to_string(), "Wrong group ID");
let error = Error::ProcessMessageUseAfterEviction;
assert_eq!(error.to_string(), "Use after eviction");
let error = Error::ProcessMessageOther("custom error".to_string());
assert_eq!(error.to_string(), "custom error");
let error = Error::ProtocolMessage("protocol error".to_string());
assert_eq!(error.to_string(), "protocol error");
let error = Error::KeyPackage("key package error".to_string());
assert_eq!(error.to_string(), "key package error");
let error = Error::Group("group error".to_string());
assert_eq!(error.to_string(), "group error");
let error = Error::GroupExporterSecretNotFound;
assert_eq!(error.to_string(), "group exporter secret not found");
let error = Error::Message("message error".to_string());
assert_eq!(error.to_string(), "message error");
let error = Error::CannotDecryptOwnMessage;
assert_eq!(error.to_string(), "cannot decrypt own message");
let error = Error::MergePendingCommit("merge error".to_string());
assert_eq!(error.to_string(), "merge error");
let error = Error::CommitToPendingProposalsError;
assert_eq!(error.to_string(), "unable to commit to pending proposal");
let error = Error::SelfUpdate("self update error".to_string());
assert_eq!(error.to_string(), "self update error");
let error = Error::Welcome("welcome error".to_string());
assert_eq!(error.to_string(), "welcome error");
let error = Error::WelcomePreviouslyFailed("original error reason".to_string());
assert_eq!(
error.to_string(),
"welcome previously failed to process: original error reason"
);
let error = Error::ProcessedWelcomeNotFound;
assert_eq!(error.to_string(), "processed welcome not found");
let error = Error::Provider("provider error".to_string());
assert_eq!(error.to_string(), "provider error");
let error = Error::GroupNotFound;
assert_eq!(error.to_string(), "group not found");
let error = Error::ProtocolGroupIdMismatch;
assert_eq!(
error.to_string(),
"protocol message group ID doesn't match the current group ID"
);
let error = Error::OwnLeafNotFound;
assert_eq!(error.to_string(), "own leaf not found");
let error = Error::CantLoadSigner;
assert_eq!(error.to_string(), "can't load signer");
let error = Error::InvalidWelcomeMessage;
assert_eq!(error.to_string(), "invalid welcome message");
let error = Error::UnexpectedExtensionType;
assert_eq!(error.to_string(), "Unexpected extension type");
let error = Error::NostrGroupDataExtensionNotFound;
assert_eq!(error.to_string(), "Nostr group data extension not found");
let error = Error::MessageFromNonMember;
assert_eq!(error.to_string(), "Message received from non-member");
let error = Error::NotImplemented("feature X".to_string());
assert_eq!(error.to_string(), "feature X");
let error = Error::MessageNotFound;
assert_eq!(error.to_string(), "stored message not found");
let error = Error::CommitFromNonAdmin;
assert_eq!(error.to_string(), "not processing commit from non-admin");
let error = Error::UpdateGroupContextExts("context error".to_string());
assert_eq!(
error.to_string(),
"Error when updating group context extensions context error"
);
let error = Error::InvalidImageHashLength;
assert_eq!(error.to_string(), "invalid image hash length");
let error = Error::InvalidImageKeyLength;
assert_eq!(error.to_string(), "invalid image key length");
let error = Error::InvalidImageNonceLength;
assert_eq!(error.to_string(), "invalid image nonce length");
let error = Error::InvalidImageUploadKeyLength;
assert_eq!(error.to_string(), "invalid image upload key length");
let error = Error::InvalidExtensionVersion(99);
assert_eq!(error.to_string(), "invalid extension version: 99");
let error = Error::AuthorMismatch;
assert_eq!(
error.to_string(),
"author mismatch: rumor pubkey does not match MLS sender"
);
let error = Error::MissingRumorEventId;
assert_eq!(error.to_string(), "rumor event is missing its ID");
}
#[test]
fn test_unexpected_event_error() {
let error = Error::UnexpectedEvent {
expected: Kind::MlsGroupMessage,
received: Kind::TextNote,
};
let msg = error.to_string();
assert!(msg.contains("unexpected event kind"));
assert!(msg.contains("expected="));
assert!(msg.contains("received="));
}
#[test]
fn test_key_package_identity_mismatch_error() {
let error = Error::KeyPackageIdentityMismatch {
credential_identity: "abc123".to_string(),
event_signer: "def456".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("key package identity mismatch"));
assert!(msg.contains("abc123"));
assert!(msg.contains("def456"));
}
#[test]
fn test_identity_change_not_allowed_error() {
let error = Error::IdentityChangeNotAllowed {
original_identity: "original_id".to_string(),
new_identity: "new_id".to_string(),
};
let msg = error.to_string();
assert!(msg.contains("identity change not allowed"));
assert!(msg.contains("original_id"));
assert!(msg.contains("new_id"));
}
#[test]
fn test_error_equality() {
let error1 = Error::GroupNotFound;
let error2 = Error::GroupNotFound;
let error3 = Error::OwnLeafNotFound;
assert_eq!(error1, error2);
assert_ne!(error1, error3);
let error1 = Error::Message("test".to_string());
let error2 = Error::Message("test".to_string());
let error3 = Error::Message("different".to_string());
assert_eq!(error1, error2);
assert_ne!(error1, error3);
}
#[test]
fn test_from_utf8_error_conversion() {
let invalid_bytes = vec![0xff, 0xfe];
let utf8_result = String::from_utf8(invalid_bytes);
assert!(utf8_result.is_err());
let error: Error = utf8_result.unwrap_err().into();
assert!(matches!(error, Error::Utf8(_)));
}
#[test]
fn test_error_debug() {
let error = Error::GroupNotFound;
let debug_str = format!("{:?}", error);
assert!(debug_str.contains("GroupNotFound"));
let error = Error::UnexpectedEvent {
expected: Kind::MlsGroupMessage,
received: Kind::TextNote,
};
let debug_str = format!("{:?}", error);
assert!(debug_str.contains("UnexpectedEvent"));
}
#[test]
fn test_process_message_wrong_epoch() {
let error = Error::ProcessMessageWrongEpoch(42, true);
assert_eq!(
error.to_string(),
"Message epoch differs from the group's epoch"
);
let error2 = Error::ProcessMessageWrongEpoch(100, false);
assert_eq!(error.to_string(), error2.to_string());
}
#[test]
fn test_own_commit_pending() {
let error = Error::OwnCommitPending;
assert_eq!(error.to_string(), "own commit pending merge");
}
#[test]
fn test_snapshot_creation_failed() {
let error = Error::SnapshotCreationFailed("storage unavailable".to_string());
assert_eq!(
error.to_string(),
"failed to create epoch snapshot: storage unavailable"
);
}
#[test]
fn test_storage_error_conversion() {
use mdk_storage_traits::MdkStorageError;
let storage_error = MdkStorageError::NotFound("group not found".to_string());
let error: Error = storage_error.into();
assert!(matches!(error, Error::Storage(_)));
let msg = error.to_string();
assert!(msg.contains("not found"));
}
}