rsasl 2.0.0-rc.4

The Rust SASL framework, aimed at both middleware-style protocol implementation and application code. Designed to make SASL authentication simple and safe while handing as much control to the user as possible.
Documentation
use core::any::type_name;
use core::fmt;

use crate::callback::{Action, CallbackError, ClosureCR, Request, Satisfy, SessionCallback};
use crate::channel_bindings::ChannelBindingCallback;
use crate::context::{build_context, Provider, ProviderExt, ThisProvider};
use crate::error::SessionError;
use crate::property::{ChannelBindingName, ChannelBindings, Property};
use crate::registry::Mechanism;
use crate::typed::{tags, Tagged};
use crate::validate::{Validate, ValidationError};

#[allow(clippy::exhaustive_enums)]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Side {
    Client,
    Server,
}

#[cfg(any(feature = "provider", feature = "testutils", test))]
mod provider {
    use super::{
        ChannelBindingCallback, Mechanism, MechanismData, SessionCallback, SessionData,
        SessionError, Side, State, Validate,
    };
    use crate::alloc::boxed::Box;
    use crate::channel_bindings::NoChannelBindings;
    use crate::mechanism::Authentication;
    use crate::mechname::Mechname;
    use crate::sasl::Sasl;
    use crate::validate::{NoValidation, Validation};
    use acid_io::Write;

    /// This represents a single authentication exchange
    ///
    /// An authentication exchange may have multiple steps, with each step potentially sending data
    /// to the other party and/or receiving data from the other party.
    ///
    /// On a server-side session after a `Finished` is received validation data from the user
    /// callback may be extracted with a call to [`Session::validation`].
    pub struct Session<V: Validation = NoValidation, C = NoChannelBindings> {
        sasl: Sasl<V, C>,
        side: Side,
        mechanism: Box<dyn Authentication>,
        mechanism_desc: Mechanism,
    }

    impl<V: Validation, C: ChannelBindingCallback> Session<V, C> {
        pub(crate) fn new(
            sasl: Sasl<V, C>,
            side: Side,
            mechanism: Box<dyn Authentication>,
            mechanism_desc: Mechanism,
        ) -> Self {
            Self {
                sasl,
                side,
                mechanism,
                mechanism_desc,
            }
        }

        #[inline(always)]
        /// Return `true` if this side of the authentication exchange should go first.
        ///
        /// Mechanisms in SASL may be either "server-first" or "client-first" indicating which side
        /// needs to send the first message in an authentication exchange.
        ///
        /// For example:
        /// `PLAIN` is a client-first mechanism. The client sends the first message, containing the
        /// username and password in plain text. `DIGEST-MD5` on the other hand is server-first, an
        /// authentication begins with a server sending a 'challenge' to the client.
        ///
        /// This method returns if the current side must go first, i.e. if this method returns `true`
        /// then `step` or `step64` must be called with no input data to begin the authentication. If
        /// this method returns `false` then the first call to `step` or `step64` can only be
        /// performed after input data was received from the other party.
        pub fn are_we_first(&self) -> bool {
            self.side == self.mechanism_desc.first
        }

        /// Return the name of the mechanism in use
        pub const fn get_mechname(&self) -> &Mechname {
            self.mechanism_desc.mechanism
        }

        /// Perform one step of SASL authentication.
        ///
        /// *requires feature `provider`*
        ///
        /// A protocol implementation calls this method with data provided by the other party,
        /// returning response data written to the other party until after a [`State::Finished`] is
        /// returned.
        ///  **Note:** If the other side indicates a completed authentication and sends no further
        /// authentication data but the last call to `step` returned `State::Running` you **MUST**
        /// call `step` a final time with a `None` input!
        /// This is critical to upholding all security guarantees that different mechanisms offer.
        ///
        /// SASL itself can usually not tell you if an authentication was successful or not,
        /// instead this is done by the protocol itself.
        ///
        /// If the current side is going first, generate the first batch of data by calling this
        /// method with an input of `None`. Whether or not the current side is expected to go
        /// first can be checked with [`Session::are_we_first`].
        ///
        /// Not all protocols support both client-first and server-first Mechanisms, i.e. mechanisms in
        /// which the client sends the first batch of data and mechanisms in which the server sends
        /// the first batch of data. Refer to the documentation of the protocol in question on how to
        /// indicate to the other party that they have to provide the first batch of data.
        ///
        /// Data generated by a mechanism **MUST** be sent even if `step` returned
        /// `State::Finished`. This means that when `Ok(State::Finished(MessageSent::Yes)` is
        /// returned from `step` a final response **MUST** be sent to the other side to finish the
        /// authentication. This is true even no bytes were written into the provided writer. In
        /// that case a final empty response must be sent to the other party.
        pub fn step(
            &mut self,
            input: Option<&[u8]>,
            writer: &mut impl Write,
        ) -> Result<State, SessionError> {
            let state = {
                let validate = Validate::new::<V>(&mut self.sasl.validation);
                let mut mechanism_data = MechanismData::new(
                    self.sasl.config.get_callback(),
                    &self.sasl.cb,
                    validate,
                    self.mechanism_desc,
                    self.side,
                );
                if let Some(input) = input {
                    self.mechanism
                        .step(&mut mechanism_data, Some(input), writer)
                } else {
                    self.mechanism.step(&mut mechanism_data, None, writer)
                }?
            };

            Ok(state)
        }

        /// Extract the [`Validation`] result of an authentication exchange
        ///
        /// This are useful to e.g. indicate success or failure of the authentication exchange and
        /// supply the protocol crate with information about the user that was authenticated.
        ///
        /// Validation results are generated by the user-supplied callback. They thus allow to send
        /// information from the callback to the protocol implementation. The type of this information
        /// can be freely defined by said implementation, but due to required type erasure inside rsasl
        /// the type must be exposed to the downstream user. Further details regarding this mechanic
        /// can be found in the [`validate`](crate::validate) module documentation.
        ///
        /// This method will most likely return `None` until `step` has returned with
        /// `State::Finished`, but it not guaranteed to do so.
        pub fn validation(&mut self) -> Option<V::Value> {
            self.sasl.validation.take()
        }
    }

    #[cfg(feature = "provider_base64")]
    impl<V: Validation, C: ChannelBindingCallback> Session<V, C> {
        /// Perform one step of SASL authentication, base64 encoded.
        ///
        /// *requires feature `provider_base64`*
        ///
        /// This is a utility function wrapping [`Session::step`] to consume and produce
        /// base64-encoded data. See the documentation of `step` for details on how this function
        /// operates and how to handle the different returned values.
        ///
        /// Requiring base64-encoded SASL data is common in line-based or textual formats, such as
        /// SMTP, IMAP, XMPP and IRCv3.
        /// Refer to your protocol documentation if SASL data needs to be base64 encoded.
        pub fn step64(
            &mut self,
            input: Option<&[u8]>,
            writer: &mut impl Write,
        ) -> Result<State, SessionError> {
            use base64::write::EncoderWriter;
            let mut writer64 = EncoderWriter::new(writer, base64::STANDARD);

            let state = if let Some(input) = input {
                let input = base64::decode_config(input, base64::STANDARD)?;
                self.step(Some(&input[..]), &mut writer64)
            } else {
                self.step(None, &mut writer64)
            }?;
            Ok(state)
        }
    }

    impl<'a> MechanismData<'a> {
        fn new(
            callback: &'a dyn SessionCallback,
            chanbind_cb: &'a dyn ChannelBindingCallback,
            validator: &'a mut Validate<'a>,
            mechanism_desc: Mechanism,
            side: Side,
        ) -> Self {
            Self {
                callback,
                chanbind_cb,
                validator,
                session_data: SessionData::new(mechanism_desc, side),
            }
        }
    }

    impl SessionData {
        pub(crate) const fn new(mechanism_desc: Mechanism, side: Side) -> Self {
            Self {
                mechanism_desc,
                side,
            }
        }
    }

    #[cfg(test)]
    pub mod tests {
        use super::*;
        use crate::context::EmptyProvider;
        use crate::validate::Validation;

        impl<V: Validation, CB: ChannelBindingCallback> Session<V, CB> {
            pub fn get_cb_data<'a, F, G>(
                &'a self,
                cbname: &str,
                validate: &'a mut Validate<'a>,
                f: &mut F,
            ) -> Result<G, SessionError>
            where
                F: FnMut(&[u8]) -> Result<G, SessionError>,
            {
                let mechanism_data = MechanismData::new(
                    self.sasl.config.get_callback(),
                    &self.sasl.cb,
                    validate,
                    self.mechanism_desc,
                    self.side,
                );
                mechanism_data.need_cb_data(cbname, EmptyProvider, f)
            }
        }
    }
}
#[cfg(any(feature = "provider", feature = "testutils", test))]
pub use provider::Session;

pub struct MechanismData<'a> {
    callback: &'a dyn SessionCallback,
    chanbind_cb: &'a dyn ChannelBindingCallback,
    validator: &'a mut Validate<'a>,
    session_data: SessionData,
}

impl MechanismData<'_> {
    pub fn validate(&mut self, provider: &dyn Provider) -> Result<(), ValidationError> {
        let context = build_context(provider);
        self.callback
            .validate(&self.session_data, context, self.validator)
    }

    fn callback(
        &self,
        provider: &dyn Provider,
        request: &mut Request<'_>,
    ) -> Result<(), SessionError> {
        let context = build_context(provider);
        match self.callback.callback(&self.session_data, context, request) {
            Ok(()) | Err(SessionError::CallbackError(CallbackError::EarlyReturn(_))) => Ok(()),
            Err(e) => Err(e),
        }
    }

    pub fn action<'a, T>(
        &self,
        provider: &dyn Provider,
        value: &'a T::Value,
    ) -> Result<(), SessionError>
    where
        T: Property<'a>,
    {
        let mut tagged = Tagged::<'a, Action<T>>(Some(value));
        self.callback(provider, Request::new_action::<T>(&mut tagged))?;
        if tagged.is_some() {
            Err(SessionError::CallbackError(CallbackError::NoCallback(
                type_name::<T>(),
            )))
        } else {
            Ok(())
        }
    }

    pub fn need_with<P, F, G>(&self, provider: &dyn Provider, closure: F) -> Result<G, SessionError>
    where
        P: for<'p> Property<'p>,
        F: FnOnce(&<P as Property<'_>>::Value) -> Result<G, SessionError>,
    {
        self.maybe_need_with::<P, F, G>(provider, closure)?
            .ok_or_else(|| CallbackError::NoCallback(type_name::<P>()).into())
    }

    pub fn maybe_need_with<P, F, G>(
        &self,
        provider: &dyn Provider,
        closure: F,
    ) -> Result<Option<G>, SessionError>
    where
        P: for<'p> Property<'p>,
        F: FnOnce(&<P as Property<'_>>::Value) -> Result<G, SessionError>,
    {
        let mut closurecr = ClosureCR::<P, _, _>::wrap(closure);
        let mut tagged = Tagged::<'_, tags::RefMut<Satisfy<P>>>(&mut closurecr);
        match self.callback(provider, Request::new_satisfy::<P>(&mut tagged)) {
            // explicitly ignore a `NoValue` error since that one *is actually okay*
            Ok(()) | Err(SessionError::CallbackError(CallbackError::NoValue)) => Ok(()),
            Err(error) => Err(error),
        }?;
        Ok(closurecr.try_unwrap())
    }

    pub fn need_cb_data<'a, P, F, G>(
        &self,
        cbname: &'a str,
        provider: P,
        f: F,
    ) -> Result<G, SessionError>
    where
        P: Provider<'a>,
        F: FnOnce(&[u8]) -> Result<G, SessionError>,
    {
        let prov = ThisProvider::<ChannelBindingName>::with(cbname).and(provider);
        if let Some(cbdata) = self.chanbind_cb.get_cb_data(cbname) {
            f(cbdata)
        } else {
            self.maybe_need_with::<ChannelBindings, F, G>(&prov, f)?
                .ok_or_else(|| SessionError::MissingChannelBindingData(cbname.to_string()))
        }
    }
}

#[derive(Debug)]
// TODO: Since the Session object is only known to the protocol implementation and user they can
//       share a statically known Context.
pub struct SessionData {
    mechanism_desc: Mechanism,
    side: Side,
}
impl SessionData {
    #[must_use]
    pub const fn mechanism(&self) -> &Mechanism {
        &self.mechanism_desc
    }

    #[must_use]
    pub const fn side(&self) -> Side {
        self.side
    }
}

impl fmt::Debug for MechanismData<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SessionData").finish()
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
/// State result of the underlying Mechanism implementation
pub enum State {
    /// The Mechanism has not yet completed the authentication exchange
    ///
    /// If this is returned the mechanism has written a message to be sent to the other
    /// party into the provided writer and is expecting a response.
    Running,

    /// The Mechanism has received all required information from the other party.
    ///
    /// However, a Mechanism returning `Finished` may still have *written* data. This data MUST be
    /// sent to the other party to ensure both sides have received all required data. The fact if
    /// a message is to be sent is indicated by the contained [`MessageSent`].
    ///
    /// After a `Finished` is returned `step` or `step64` MUST NOT be called further.
    ///
    /// **NOTE**: This state does not guarantee that the authentication was *successful*, only that
    /// no further calls to `step` or `step64` are possible.
    /// Most SASL mechanisms have no way of returning the authentication outcome inline.
    /// Instead the outer protocol will indicate the authentication outcome in a protocol-specific
    /// way.
    Finished(MessageSent),
}
impl State {
    #[inline(always)]
    #[must_use]
    pub const fn is_running(&self) -> bool {
        matches!(self, Self::Running)
    }

    #[inline(always)]
    #[must_use]
    pub const fn is_finished(&self) -> bool {
        !self.is_running()
    }

    #[inline(always)]
    #[must_use]
    pub const fn has_sent_message(&self) -> bool {
        matches!(self, Self::Running | Self::Finished(MessageSent::Yes))
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
/// Indication if a message was written into the provided writer.
///
/// This enum is returned by a call to `step` or `step64` and indicates if a message was written
/// into the provided writer. It serves as a hint to a caller to inform them that they need to
/// ensure the message will reach the other party, be that by flushing the writer or copying the
/// written bytes.
///
/// Note that SASL explicitly allows the option of sending an *empty* message. In that case a
/// `MessageSent::Yes` will be returned but no bytes will have been written into the writer. How
/// to indicate an empty message differs from protocol to protocol.
pub enum MessageSent {
    /// Yes a message was written and needs to be sent
    Yes,
    /// No message needs to be sent to the other end.
    No,
}

#[cfg(test)]
mod tests {
    use super::*;

    static_assertions::assert_impl_all!(Session: Send, Sync);
}