core_crypto_ffi/error/
core_crypto.rs

1use core_crypto::{InnermostErrorMessage as _, RecursiveError};
2
3#[cfg(feature = "proteus")]
4use crate::ProteusError;
5use crate::{MlsError, error::log_error};
6#[cfg(target_family = "wasm")]
7use wasm_bindgen::JsValue;
8
9#[derive(Debug, thiserror::Error)]
10#[cfg_attr(target_family = "wasm", derive(strum::AsRefStr))]
11#[cfg_attr(not(target_family = "wasm"), derive(uniffi::Error))]
12pub enum CoreCryptoError {
13    #[error(transparent)]
14    Mls(#[from] MlsError),
15    #[cfg(feature = "proteus")]
16    #[error(transparent)]
17    Proteus(#[from] ProteusError),
18    #[error("End to end identity error: {0}")]
19    E2ei(String),
20    #[cfg(target_family = "wasm")]
21    #[error(transparent)]
22    SerializationError(#[from] serde_wasm_bindgen::Error),
23    #[cfg(target_family = "wasm")]
24    #[error("Unknown ciphersuite identifier")]
25    UnknownCiphersuite,
26    #[cfg(target_family = "wasm")]
27    #[error("Transaction rolled back due to unexpected JS error: {0:?}")]
28    TransactionFailed(JsValue),
29    #[cfg(not(target_family = "wasm"))]
30    #[error("Transaction rolled back due to unexpected uniffi error: {0:?}")]
31    TransactionFailed(String),
32    #[error("{0}")]
33    Other(String),
34}
35
36#[cfg(not(target_family = "wasm"))]
37impl From<uniffi::UnexpectedUniFFICallbackError> for CoreCryptoError {
38    fn from(value: uniffi::UnexpectedUniFFICallbackError) -> Self {
39        Self::TransactionFailed(value.to_string())
40    }
41}
42
43impl From<RecursiveError> for CoreCryptoError {
44    fn from(error: RecursiveError) -> Self {
45        log_error(&error);
46
47        let innermost = {
48            let mut err: &dyn std::error::Error = &error;
49            while let Some(inner) = err.source() {
50                err = inner;
51            }
52            err
53        };
54
55        // check if the innermost error is any kind of e2e error
56        if let Some(err) = innermost.downcast_ref::<core_crypto::e2e_identity::Error>() {
57            return CoreCryptoError::E2ei(err.to_string());
58        }
59
60        // What now? We only really care about the innermost variants, not the error stack, but that produces
61        // an arbitrary set of types. We can't match against that!
62        //
63        // Or at least, not without the power of macros. We can use them to match against heterogenous types.
64
65        /// Like [`matches!`], but with an out expression which can reference items captured by the pattern.
66        ///
67        /// Hopefully only ever use this in conjunction with `interior_matches!`, because for most sane
68        /// circumstances, `if let` is the better design pattern.
69        macro_rules! matches_option {
70            // Without cfg attribute
71            (
72                $val:expr,
73                $pattern:pat $(if $guard:expr)? => $out:expr
74            ) => {
75                match ($val) {
76                    $pattern $(if $guard)? => Some($out),
77                    _ => None,
78                }
79            };
80            // With cfg attribute
81            (
82                $val:expr,
83                #[cfg($meta:meta)]
84                $pattern:pat $(if $guard:expr)? => $out:expr
85            ) => {
86                {
87                    #[cfg($meta)]
88                    let result = match ($val) {
89                        $pattern $(if $guard)? => Some($out),
90                        _ => None,
91                    };
92                    #[cfg(not($meta))]
93                    let result = None;
94                    result
95                }
96            };
97        }
98
99        /// This is moderately horrific and we hopefully will not require it anywhere else, but
100        /// it solves a real problem here: how do we match against the innermost error variants,
101        /// when we have a heterogenous set of types to match against?
102        macro_rules! match_heterogenous {
103            (
104                $err:expr => {
105                    $(
106                        $( #[cfg($meta:meta)] )?
107                        $pattern:pat $(if $guard:expr)? => $var:expr,
108                    )*
109                    ||=> $default:expr,
110                }
111            ) => {{
112                if false { unreachable!() }
113                $(
114                    else if let Some(v) = matches_option!(
115                        $err.downcast_ref(),
116                        $( #[cfg($meta)] )?
117                        Some($pattern) $(if $guard)? => $var
118                    ) {
119                        v
120                    }
121                )*
122                else {
123                    $default
124                }
125            }};
126        }
127
128        match_heterogenous!(innermost => {
129            core_crypto::LeafError::ConversationAlreadyExists(id) => MlsError::ConversationAlreadyExists(id.clone()).into(),
130            core_crypto::mls::conversation::Error::BufferedFutureMessage{..} => MlsError::BufferedFutureMessage.into(),
131            core_crypto::mls::conversation::Error::DuplicateMessage => MlsError::DuplicateMessage.into(),
132            core_crypto::mls::conversation::Error::MessageEpochTooOld => MlsError::MessageEpochTooOld.into(),
133            core_crypto::mls::conversation::Error::SelfCommitIgnored => MlsError::SelfCommitIgnored.into(),
134            core_crypto::mls::conversation::Error::StaleCommit => MlsError::StaleCommit.into(),
135            core_crypto::mls::conversation::Error::StaleProposal => MlsError::StaleProposal.into(),
136            core_crypto::mls::conversation::Error::UnbufferedFarFutureMessage => MlsError::WrongEpoch.into(),
137            core_crypto::mls::conversation::Error::BufferedCommit => MlsError::BufferedCommit.into(),
138            core_crypto::mls::conversation::Error::MessageRejected { reason } => MlsError::MessageRejected { reason: reason.clone() }.into(),
139            core_crypto::mls::conversation::Error::OrphanWelcome => MlsError::OrphanWelcome.into(),
140            #[cfg(feature="proteus")]
141            e @ (
142                core_crypto::ProteusErrorKind::ProteusDecodeError(_)
143                | core_crypto::ProteusErrorKind::ProteusEncodeError(_)
144                | core_crypto::ProteusErrorKind::ProteusInternalError(_)
145                | core_crypto::ProteusErrorKind::ProteusSessionError(_)
146                | core_crypto::ProteusErrorKind::Leaf(_)
147            ) => ProteusError::from(e).into(),
148            // The internal name is what we want, but renaming the external variant is a breaking change.
149            // Since we're re-designing the `BufferedMessage` errors soon, it's not worth producing
150            // an additional breaking change until then, so the names are inconsistent.
151            core_crypto::mls::conversation::Error::BufferedForPendingConversation => MlsError::UnmergedPendingGroup.into(),
152            ||=> MlsError::Other(error.innermost_error_message()).into(),
153        })
154    }
155}
156
157// This implementation is intended to be temporary; we're going to be completely restructuring the way we handle
158// errors in `core-crypto` soon. We can replace this with better error patterns when we do.
159//
160// Certain error mappings could apply to both MLS and Proteus. In all such cases, we map them to the MLS variant.
161// When we redesign the errors in `core-crypto`, these ambiguities should disappear anyway.
162impl From<core_crypto::Error> for CoreCryptoError {
163    fn from(error: core_crypto::Error) -> Self {
164        log_error(&error);
165
166        match error {
167            #[cfg(feature = "proteus")]
168            core_crypto::Error::ProteusNotInitialized => Self::Other(error.to_string()),
169            #[cfg(not(feature = "proteus"))]
170            core_crypto::Error::ProteusNotInitialized => Self::Other("proteus not initialized".into()),
171            #[cfg(feature = "proteus")]
172            core_crypto::Error::Proteus(proteus) => {
173                let error_code = proteus.source.error_code();
174                if let Some(proteus_error) = ProteusError::from_error_code(error_code) {
175                    Self::Proteus(proteus_error)
176                } else {
177                    Self::Other(format!("unknown proteus error code: {error_code:?}"))
178                }
179            }
180            #[cfg(not(feature = "proteus"))]
181            core_crypto::Error::Proteus(_proteus) => {
182                unreachable!("we don't raise proteus errors when building without proteus")
183            }
184            core_crypto::Error::Mls(mls) => Self::Mls(MlsError::from(mls)),
185            core_crypto::Error::InvalidTransactionContext => Self::Other(error.to_string()),
186            core_crypto::Error::MlsTransportNotProvided => Self::Other(error.to_string()),
187            core_crypto::Error::ErrorDuringMlsTransport(error_message) => Self::Other(error_message),
188            core_crypto::Error::Keystore(keystore_error) => Self::Other(keystore_error.innermost_error_message()),
189            core_crypto::Error::CryptoboxMigration(cryptobox) => Self::Other(cryptobox.innermost_error_message()),
190            core_crypto::Error::Recursive(recursive_error) => recursive_error.into(),
191            core_crypto::Error::FeatureDisabled(_) => Self::Other(error.to_string()),
192        }
193    }
194}
195
196/// We can't do a generic `impl<E: ToRecursiveError> From<E> for CoreCryptoError`
197/// because that has the potential to cause breaking conflicts later on: what if
198/// core-crypto later did `impl ToRecursiveError for core_crypto::Error`? That would
199/// cause a duplicate `From` impl.
200///
201/// Instead, we explicitly specify every variant which can be converted to a
202/// `CoreCryptoError`, and implement its `From` block directly.
203macro_rules! impl_from_via_recursive_error {
204    ($($t:ty),+ $(,)?) => {
205        $(
206            impl From<$t> for CoreCryptoError {
207                fn from(error: $t) -> Self {
208                    use core_crypto::ToRecursiveError;
209                    error
210                        .construct_recursive("this context string does not matter and gets immediately stripped")
211                        .into()
212                }
213            }
214        )*
215    };
216}
217
218impl_from_via_recursive_error!(
219    core_crypto::mls::Error,
220    core_crypto::mls::conversation::Error,
221    core_crypto::e2e_identity::Error,
222    core_crypto::transaction_context::Error,
223);
224
225impl CoreCryptoError {
226    pub(crate) fn generic<E>() -> impl FnOnce(E) -> Self
227    where
228        E: ToString,
229    {
230        |err| Self::Other(err.to_string())
231    }
232
233    pub(crate) fn ad_hoc(err: impl ToString) -> Self {
234        Self::Other(err.to_string())
235    }
236
237    #[cfg(target_family = "wasm")]
238    pub(crate) fn variant_name(&self) -> String {
239        let mut out = self.as_ref().to_string() + "Error";
240        match self {
241            Self::Mls(mls) => out += mls.as_ref(),
242            Self::Proteus(proteus) => out += proteus.as_ref(),
243            _ => {}
244        }
245        out
246    }
247
248    #[cfg(target_family = "wasm")]
249    pub(crate) fn stack(&self) -> Vec<String> {
250        let mut stack = Vec::new();
251        let mut err: &dyn std::error::Error = self;
252        stack.push(err.to_string());
253
254        while let Some(source) = err.source() {
255            stack.push(source.to_string());
256            err = source;
257        }
258
259        stack
260    }
261}