Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/libthreema/lib/src/common/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 34 kB image not shown  

Quelle  mod.rs   Sprache: Rust

 

//! Common items needed in many places.
use core::{
    array::TryFromSliceError,
    fmt,
    num::ParseIntError,
    str::{self, FromStr},
};
use std::env;

#[cfg(test)]
use anyhow;
use libthreema_dependencies::{
    crypto::{
        consts,
        digest::{FixedOutput as _, Mac as _},
        generic_array, sha2,
    },
    data_encoding::HEXLOWER_PERMISSIVE,
    rand::{self, Rng as _},
};
use libthreema_macros::{Name, UniffiNewType, VariantNames};
use serde::{Deserialize, Serialize};

use crate::{
    protobuf,
    utils::{debug::Name as _, protobuf::PaddedMessage as _, time::utc_now_ms, update::Apply},
};

pub mod config;
pub mod keys;
pub mod task;

/// Client info.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(
    feature = "wasm",
    derive(tsify::Tsify, serde::Deserialize),
    serde(tag = "platform", rename_all = "kebab-case", rename_all_fields = "camelCase"),
    tsify(from_wasm_abi)
)]
pub enum ClientInfo {
    /// Android.
    Android {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Device model.
        device_model: String,

        /// OS version.
        os_version: String,
    },

    /// iOS.
    Ios {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Device model.
        device_model: String,

        /// OS version.
        os_version: String,
    },

    /// Desktop 2.x.
    Desktop {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Renderer name (e.g. `electron`).
        renderer_name: String,

        /// Renderer version.
        renderer_version: String,

        /// OS name (e.g. `linux`).
        os_name: String,

        /// OS architecture (e.g. `x64`).
        os_architecture: String,
    },

    /// libthreema standalone (CLI, testing, ...)
    Libthreema,
}
impl ClientInfo {
    /// Encode to HTTPS user agent name.
    #[must_use]
    pub fn to_user_agent(&self) -> String {
        match self {
            Self::Android { version, .. } => format!("Threema Android/{version}"),
            Self::Ios { version, .. } => format!("Threema iOS/{version}"),
            Self::Desktop { version, .. } => format!("Threema Desktop/{version}"),
            Self::Libthreema => format!("libthreema/{version}", version = env!("CARGO_PKG_VERSION")),
        }
    }

    /// Encode to colon-separated as used by the CSP `client-info` (hopefully not for long).
    #[must_use]
    pub fn to_semicolon_separated(&self) -> String {
        match self {
            Self::Android {
                version,
                locale,
                device_model,
                os_version,
            } => {
                format!(
                    "{version};A;{locale};{device_model};{os_version}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    device_model = device_model.replace(';', "_"),
                    os_version = os_version.replace(';', "_")
                )
            },

            Self::Ios {
                version,
                locale,
                device_model,
                os_version,
            } => {
                format!(
                    "{version};I;{locale};{device_model};{os_version}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    device_model = device_model.replace(';', "_"),
                    os_version = os_version.replace(';', "_")
                )
            },

            Self::Desktop {
                version,
                locale,
                renderer_name,
                renderer_version,
                os_name,
                os_architecture,
            } => {
                format!(
                    "{version};Q;{locale};{renderer_name};{renderer_version};{os_name};{os_architecture}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    renderer_name = renderer_name.replace(';', "_"),
                    renderer_version = renderer_version.replace(';', "_"),
                    os_name = os_name.replace(';', "_"),
                    os_architecture = os_architecture.replace(';', "_")
                )
            },

            Self::Libthreema => {
                format!(
                    "{version};L;en/CH;{os_name};{os_architecture}",
                    version = env!("CARGO_PKG_VERSION"),
                    os_name = env::consts::OS,
                    os_architecture = env::consts::ARCH
                )
            },
        }
    }

    /// Construct a [`protobuf::d2d::DeviceInfo`] from a device label and the [`ClientInfo`].
    ///
    /// The device label (e.g. "PC at Work") is recommended to not exceed 64 grapheme clusters.
    pub(crate) fn to_device_info(&self, label: String) -> protobuf::d2d::DeviceInfo {
        let (platform, platform_details, app_version) = match self {
            Self::Android {
                version,
                device_model,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Android,
                device_model.clone(),
                version.clone(),
            ),

            Self::Ios {
                version,
                device_model,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Ios,
                device_model.clone(),
                version.clone(),
            ),

            Self::Desktop {
                version,
                renderer_name,
                renderer_version,
                os_name,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Desktop,
                format!("{renderer_name} {renderer_version} ({os_name})"),
                version.clone(),
            ),

            Self::Libthreema => (
                protobuf::d2d::device_info::Platform::Unspecified,
                format!("libthreema ({os_name})", os_name = env::consts::OS),
                env!("CARGO_PKG_VERSION").to_owned(),
            ),
        };
        protobuf::d2d::DeviceInfo {
            #[expect(deprecated, reason = "Will be filled by encode_to_vec_padded")]
            padding: vec![],
            platform: platform as i32,
            platform_details,
            app_version,
            label,
        }
    }
}

/// Invalid [`ThreemaId`].
#[derive(Clone, Debug, thiserror::Error)]
pub enum ThreemaIdError {
    /// Invalid length (must be exactly 8 bytes).
    #[error("Threema ID must be exactly 8 bytes")]
    InvalidLength,

    /// Invalid symbols provided.
    #[error("Threema ID contains invalid symbols")]
    InvalidSymbols,
}
#[cfg(feature = "uniffi")]
impl From<ThreemaIdError> for crate::bindings::uniffi::ConversionError {
    fn from(error: ThreemaIdError) -> Self {
        Self::ContractViolation {
            name: ThreemaId::NAME.to_owned(),
            reason: error.to_string(),
        }
    }
}

/// A valid Threema ID.
#[expect(
    clippy::unsafe_derive_deserialize,
    reason = "False positive triggered by the unsafe block in as_str, \
    see https://github.com/rust-lang/rust-clippy/issues/10349"
)]
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize, Name)]
#[serde(try_from = "&str", into = "String")]
pub struct ThreemaId([u8; Self::LENGTH]);
impl ThreemaId {
    /// Byte length of a Threema ID.
    pub const LENGTH: usize = 8;

    /// Construct a predefined Threema ID.
    ///
    /// IMPORTANT: This skips the validation, so `identity` must be known to be valid!
    pub(crate) const fn predefined(identity: [u8; Self::LENGTH]) -> Self {
        Self(identity)
    }

    /// Byte representation of the Threema ID.
    #[inline]
    #[must_use]
    pub fn to_bytes(self) -> [u8; Self::LENGTH] {
        self.0
    }

    /// String representation of the Threema ID.
    #[inline]
    #[must_use]
    pub fn as_str(&self) -> &str {
        // SAFETY: This is safe because the creation of a `ThreemaId` requires that it is a valid
        // UTF-8 sequence.
        unsafe { str::from_utf8_unchecked(&self.0) }
    }

    /// Return whether this is a Gateway ID.
    #[inline]
    #[must_use]
    pub fn is_gateway_id(self) -> bool {
        self.0[0] == b'*'
    }
}
impl fmt::Display for ThreemaId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}
impl fmt::Debug for ThreemaId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}
impl From<ThreemaId> for String {
    fn from(identity: ThreemaId) -> Self {
        identity.as_str().to_owned()
    }
}
impl TryFrom<&[u8]> for ThreemaId {
    type Error = ThreemaIdError;

    fn try_from(identity: &[u8]) -> Result<Self, Self::Error> {
        let identity = <[u8; Self::LENGTH]>::try_from(identity).map_err(|_| ThreemaIdError::InvalidLength)?;
        if ![b'*'..=b'*', b'0'..=b'9', b'A'..=b'Z']
            .iter()
            .any(|range| range.contains(identity.first().expect("identity must be exactly 8 bytes")))
        {
            return Err(ThreemaIdError::InvalidSymbols);
        }
        if !identity
            .get(1..8)
            .expect("identity must be exactly 8 bytes")
            .iter()
            .all(|byte| {
                [b'0'..=b'9', b'A'..=b'Z']
                    .iter()
                    .any(|range| range.contains(byte))
            })
        {
            return Err(ThreemaIdError::InvalidSymbols);
        }
        Ok(Self(identity))
    }
}
impl TryFrom<&str> for ThreemaId {
    type Error = ThreemaIdError;

    fn try_from(identity: &str) -> Result<Self, Self::Error> {
        Self::try_from(identity.as_bytes())
    }
}
impl FromStr for ThreemaId {
    type Err = ThreemaIdError;

    fn from_str(identity: &str) -> Result<Self, Self::Err> {
        Self::try_from(identity)
    }
}

#[cfg(test)]
mod threema_id_tests {
    use assert_matches::assert_matches;
    use rstest::rstest;

    use super::{ThreemaId, ThreemaIdError};

    #[rstest]
    #[case(&[0x45, 0x43, 0x48, 0x4f, 0x45, 0x43, 0x48, 0x4f])]
    #[case(&[0x2a, 0x52, 0x49, 0x43, 0x48, 0x54, 0x49, 0x47])]
    fn valid_bytes(#[case] identity: &[u8]) {
        assert!(ThreemaId::try_from(identity).is_ok());
    }

    #[rstest]
    #[case("ECHOECHO")]
    #[case("*RICHTIG")]
    fn valid_str(#[case] identity: &str) {
        assert!(ThreemaId::try_from(identity).is_ok());
    }

    #[rstest]
    #[case("")]
    #[case("ZUWENIG")]
    #[case("*NEINNEIN")]
    #[case("ECHÜECHÜ")]
    fn invalid_length(#[case] identity: &str) {
        assert_matches!(ThreemaId::try_from(identity), Err(ThreemaIdError::InvalidLength));
    }

    #[rstest]
    #[case(&[0x00, 0x9f, 0x92, 0x96, 0x00, 0x00, 0x00, 0x00])]
    #[case(&[0_u8; 8])]
    fn invalid_symbols_bytes(#[case] identity: &[u8]) {
        assert_matches!(ThreemaId::try_from(identity), Err(ThreemaIdError::InvalidSymbols));
    }

    #[rstest]
    #[case("ECH_ECH_")]
    #[case("********")]
    fn invalid_symbols_str(#[case] identity: &str) {
        assert_matches!(ThreemaId::try_from(identity), Err(ThreemaIdError::InvalidSymbols));
    }
}

/// A unique distribution list identity.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "uniffi", derive(UniffiNewType))]
pub struct DistributionListId(pub u64);
impl fmt::Display for DistributionListId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0.to_le_bytes()))
    }
}

/// A unique group identity.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Name)]
pub struct GroupIdentity {
    /// Group ID as chosen by the group's creator.
    pub group_id: u64,

    /// Threema ID of the group's creator.
    pub creator_identity: ThreemaId,
}
impl TryFrom<&protobuf::common::GroupIdentity> for GroupIdentity {
    type Error = ThreemaIdError;

    fn try_from(group_identity: &protobuf::common::GroupIdentity) -> Result<Self, Self::Error> {
        Ok(Self {
            group_id: group_identity.group_id,
            creator_identity: ThreemaId::try_from(group_identity.creator_identity.as_str())?,
        })
    }
}
impl From<&GroupIdentity> for protobuf::common::GroupIdentity {
    fn from(group_identity: &GroupIdentity) -> Self {
        Self {
            group_id: group_identity.group_id,
            creator_identity: group_identity.creator_identity.into(),
        }
    }
}
impl fmt::Display for GroupIdentity {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{}.{}",
            self.creator_identity,
            HEXLOWER_PERMISSIVE.encode(&self.group_id.to_le_bytes()),
        )
    }
}

/// A conversation type.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum ConversationType {
    /// A 1:1 conversation.
    Contact,

    /// A distribution list conversation.
    DistributionList,

    /// A group conversation.
    Group,
}

/// A specific conversation (aka _receiver_).
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Name, PartialOrd, Ord, VariantNames)]
pub enum ConversationId {
    /// A specific 1:1 contact.
    Contact(ThreemaId),

    /// A specific distribution list.
    DistributionList(DistributionListId),

    /// A specific group.
    Group(GroupIdentity),
}
impl ConversationId {
    #[inline]
    pub(crate) const fn conversation_type(&self) -> ConversationType {
        match self {
            Self::Contact(_) => ConversationType::Contact,
            Self::DistributionList(_) => ConversationType::DistributionList,
            Self::Group(_) => ConversationType::Group,
        }
    }
}
impl TryFrom<&protobuf::d2d::conversation_id::Id> for ConversationId {
    type Error = ThreemaIdError;

    fn try_from(conversation: &protobuf::d2d::conversation_id::Id) -> Result<Self, Self::Error> {
        Ok(match conversation {
            protobuf::d2d::conversation_id::Id::Contact(contact_identity) => {
                Self::Contact(ThreemaId::try_from(contact_identity.as_str())?)
            },
            protobuf::d2d::conversation_id::Id::DistributionList(distribution_list_id) => {
                Self::DistributionList(DistributionListId(*distribution_list_id))
            },
            protobuf::d2d::conversation_id::Id::Group(group_identity) => {
                Self::Group(GroupIdentity::try_from(group_identity)?)
            },
        })
    }
}
impl From<&ConversationId> for protobuf::d2d::ConversationId {
    fn from(conversation: &ConversationId) -> Self {
        let id = match conversation {
            ConversationId::Contact(contact_identity) => {
                protobuf::d2d::conversation_id::Id::Contact(contact_identity.as_str().to_owned())
            },
            ConversationId::DistributionList(distribution_list_id) => {
                protobuf::d2d::conversation_id::Id::DistributionList(distribution_list_id.0)
            },
            ConversationId::Group(group_identity) => {
                protobuf::d2d::conversation_id::Id::Group(group_identity.into())
            },
        };
        Self { id: Some(id) }
    }
}
impl fmt::Display for ConversationId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let inner: &dyn fmt::Display = match self {
            Self::Contact(id) => id,
            Self::DistributionList(id) => id,
            Self::Group(id) => id,
        };
        write!(formatter, "{}({inner})", self.variant_name())
    }
}

/// Invalid [`ChatServerGroup`].
#[derive(Clone, Debug, thiserror::Error)]
pub enum ChatServerGroupError {
    /// Invalid length (must be at least one byte).
    #[error("Chat server group must be at least one byte")]
    InvalidLength,

    /// Invalid symbols provided.
    #[error("Chat server group contains invalid symbols")]
    InvalidSymbols,
}
#[cfg(feature = "uniffi")]
impl From<ChatServerGroupError> for crate::bindings::uniffi::ConversionError {
    fn from(error: ChatServerGroupError) -> Self {
        Self::ContractViolation {
            name: ChatServerGroup::NAME.to_owned(),
            reason: error.to_string(),
        }
    }
}

/// A valid CSP server group (i.e., matching matching `[a-zA-Z0-9\-]+`).
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Name)]
#[serde(try_from = "&str")]
pub struct ChatServerGroup(String);
impl ChatServerGroup {
    /// Extracts a string slice containing the entire Chat Server Group.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}
impl TryFrom<&str> for ChatServerGroup {
    type Error = ChatServerGroupError;

    fn try_from(chat_server_group: &str) -> Result<Self, Self::Error> {
        if chat_server_group.is_empty() {
            return Err(ChatServerGroupError::InvalidLength);
        }
        if !chat_server_group
            .chars()
            .all(|symbol| symbol.is_ascii_alphanumeric() || symbol == '-')
        {
            return Err(ChatServerGroupError::InvalidSymbols);
        }
        Ok(Self(chat_server_group.to_owned()))
    }
}
impl FromStr for ChatServerGroup {
    type Err = ChatServerGroupError;

    fn from_str(chat_server_group: &str) -> Result<Self, Self::Err> {
        Self::try_from(chat_server_group)
    }
}
impl From<ChatServerGroup> for String {
    fn from(chat_server_group: ChatServerGroup) -> Self {
        chat_server_group.0
    }
}

#[cfg(test)]
mod chat_server_group_tests {
    use assert_matches::assert_matches;
    use rstest::rstest;

    use super::{ChatServerGroup, ChatServerGroupError};

    #[rstest]
    #[case("a")]
    #[case("0")]
    #[case("-")]
    #[case("ds")]
    #[case("aAzZ09")]
    #[case("some-group-42")]
    fn valid(#[case] chat_server_group: &str) {
        assert!(ChatServerGroup::try_from(chat_server_group).is_ok());
    }

    #[test]
    fn invalid_length() {
        assert_matches!(
            ChatServerGroup::try_from(""),
            Err(ChatServerGroupError::InvalidLength)
        );
    }

    #[rstest]
    #[case("group with spaces")]
    #[case("under_score")]
    #[case("café")]
    #[case("dot.dot")]
    fn invalid_symbols(#[case] chat_server_group: &str) {
        assert_matches!(
            ChatServerGroup::try_from(chat_server_group),
            Err(ChatServerGroupError::InvalidSymbols)
        );
    }
}

/// A CSP device cookie.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct DeviceCookie(pub [u8; Self::LENGTH]);
impl DeviceCookie {
    /// Byte length of a CSP device cookie.
    pub(crate) const LENGTH: usize = 16;

    #[cfg(any(test, feature = "cli"))]
    pub(crate) fn from_hex(string: &str) -> anyhow::Result<Self> {
        let bytes = HEXLOWER_PERMISSIVE.decode(string.as_bytes())?;
        let bytes: [u8; Self::LENGTH] = bytes.as_slice().try_into()?;
        Ok(Self(bytes))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for DeviceCookie {
    type Err = anyhow::Error;

    fn from_str(device_cookie: &str) -> Result<Self, Self::Err> {
        Self::from_hex(device_cookie)
    }
}
impl TryFrom<&[u8]> for DeviceCookie {
    type Error = TryFromSliceError;

    fn try_from(device_cookie: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self(<[u8; Self::LENGTH]>::try_from(device_cookie)?))
    }
}
impl fmt::Display for DeviceCookie {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0))
    }
}
impl fmt::Debug for DeviceCookie {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}

/// A D2X device ID.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct D2xDeviceId(pub u64);
#[cfg(any(test, feature = "cli"))]
impl TryFrom<&str> for D2xDeviceId {
    type Error = ParseIntError;

    fn try_from(id: &str) -> Result<Self, Self::Error> {
        Ok(Self(u64::from_str_radix(id, 16)?.to_be()))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for D2xDeviceId {
    type Err = ParseIntError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}
impl fmt::Display for D2xDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:02x}", self.0)
    }
}
impl fmt::Debug for D2xDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}({:016x})", Self::NAME, self.0.to_be())
    }
}

/// A CSP device ID.
///
/// WARNING: This should never be equal to the [`D2xDeviceId`].
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct CspDeviceId(pub u64);
impl CspDeviceId {
    /// Byte length of a CSP device ID.
    pub(crate) const LENGTH: usize = 8;
}
#[cfg(any(test, feature = "cli"))]
impl TryFrom<&str> for CspDeviceId {
    type Error = ParseIntError;

    fn try_from(id: &str) -> Result<Self, Self::Error> {
        Ok(Self(u64::from_str_radix(id, 16)?.to_be()))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for CspDeviceId {
    type Err = ParseIntError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}
impl fmt::Display for CspDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:02x}", self.0)
    }
}
impl fmt::Debug for CspDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}({:016x})", Self::NAME, self.0.to_be())
    }
}

/// A blob ID.
#[derive(Clone, Copy, Eq, PartialEq, Name)]
pub struct BlobId(pub [u8; Self::LENGTH]);
impl BlobId {
    /// Byte length of a blob ID.
    pub const LENGTH: usize = 16;
}
impl fmt::Debug for BlobId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}
impl fmt::Display for BlobId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0))
    }
}

/// CSP features supported by a device or available for a contact.
///
/// IMPORTANT: The flags determine what a device/contact is capable of, not
/// whether the settings allow for it. For example, group calls may be supported
/// but ignored if disabled in the settings.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(UniffiNewType))]
pub struct FeatureMask(pub u64);
#[rustfmt::skip]
impl FeatureMask {
    /// No features available.
    pub const NONE: u64 =                     0b_0000_0000_0000_0000;

    /// Supports voice messages.
    pub const VOICE_MESSAGE_SUPPORT: u64 =    0b_0000_0000_0000_0001;

    /// Supports groups.
    pub const GROUP_SUPPORT: u64 =            0b_0000_0000_0000_0010;

    /// Supports polls.
    pub const POLL_SUPPORT: u64 =             0b_0000_0000_0000_0100;

    /// Supports file messages.
    pub const FILE_MESSAGE_SUPPORT: u64 =     0b_0000_0000_0000_1000;

    /// Supports 1:1 audio calls.
    pub const O2O_AUDIO_CALL_SUPPORT: u64 =   0b_0000_0000_0001_0000;

    /// Supports 1:1 video calls.
    pub const O2O_VIDEO_CALL_SUPPORT: u64 =   0b_0000_0000_0010_0000;

    /// Supports forward security.
    pub const FORWARD_SECURITY_SUPPORT: u64 = 0b_0000_0000_0100_0000;

    /// Supports group calls.
    pub const GROUP_CALL_SUPPORT: u64 =       0b_0000_0000_1000_0000;

    /// Supports editing messages.
    pub const EDIT_MESSAGE_SUPPORT: u64 =     0b_0000_0001_0000_0000;

    /// Supports deleting messages.
    pub const DELETE_MESSAGE_SUPPORT: u64 =   0b_0000_0010_0000_0000;
}

/// A 24-byte nonce for use with XSalsa20Poly1305 or XChaCha20Poly1305.
#[derive(Clone, Eq, PartialEq, Hash, Name)]
pub struct Nonce(pub [u8; Self::LENGTH]);
impl Nonce {
    /// Byte length of a nonce.
    pub const LENGTH: usize = 24;

    /// Generate a random nonce.
    #[must_use]
    pub fn random() -> Self {
        let mut nonce = Self([0_u8; Self::LENGTH]);
        rand::thread_rng().fill(&mut nonce.0);
        nonce
    }

    #[cfg(test)]
    pub(crate) fn from_hex(string: &str) -> anyhow::Result<Self> {
        let bytes = HEXLOWER_PERMISSIVE.decode(string.as_bytes())?;
        let bytes: [u8; Self::LENGTH] = bytes.as_slice().try_into()?;
        Ok(Self(bytes))
    }

    /// Hash the Nonce with HMAC-SHA256 using the user's identity as key.
    #[must_use]
    pub(crate) fn hash_with(&self, user_identity: ThreemaId) -> HashedNonce {
        HashedNonce(
            sha2::HmacSha256::new_from_slice(&user_identity.to_bytes())
                .expect("HMAC can take Threema Id as key input")
                .chain_update(self.0)
                .finalize_fixed()
                .into(),
        )
    }
}
impl From<[u8; Self::LENGTH]> for Nonce {
    fn from(nonce: [u8; Self::LENGTH]) -> Self {
        Self(nonce)
    }
}
impl<'array, 'nonce: 'array> From<&'nonce Nonce> for &'array generic_array::GenericArray<u8, consts::U24> {
    fn from(nonce: &'nonce Nonce) -> Self {
        Self::from(&nonce.0)
    }
}
impl From<generic_array::GenericArray<u8, consts::U24>> for Nonce {
    fn from(nonce: generic_array::GenericArray<u8, consts::U24>) -> Self {
        Self(nonce.into())
    }
}
impl TryFrom<&[u8]> for Nonce {
    type Error = TryFromSliceError;

    fn try_from(nonce: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self::from(<[u8; Self::LENGTH]>::try_from(nonce)?))
    }
}
impl fmt::Display for Nonce {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER_PERMISSIVE.encode(&self.0))
    }
}
impl fmt::Debug for Nonce {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}

/// A nonce hashed with HMAC-SHA256 using the identity as key.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Name)]
#[cfg_attr(test, derive(Debug))]
pub struct HashedNonce(pub [u8; Self::LENGTH]);
impl HashedNonce {
    /// Byte length of a hashed nonce.
    pub const LENGTH: usize = 32;
}
impl From<[u8; Self::LENGTH]> for HashedNonce {
    fn from(hashed_nonce: [u8; Self::LENGTH]) -> Self {
        Self(hashed_nonce)
    }
}
impl TryFrom<&[u8]> for HashedNonce {
    type Error = TryFromSliceError;

    fn try_from(nonce: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self::from(<[u8; Self::LENGTH]>::try_from(nonce)?))
    }
}

/// A message ID.
///
/// May or may not be unique, depending on the context it is used for.
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord, Name)]
#[cfg_attr(feature = "uniffi", derive(UniffiNewType))]
pub struct MessageId(pub u64);
impl MessageId {
    /// Byte length of a message ID.
    pub const LENGTH: usize = 8;

    /// Generate a random message ID.
    #[must_use]
    pub fn random() -> Self {
        Self(rand::thread_rng().r#gen())
    }

    #[cfg(test)]
    pub(crate) fn from_hex(string: &str) -> Result<Self, ParseIntError> {
        Ok(Self(u64::from_str_radix(string, 16)?.to_be()))
    }

    /// Byte representation of the Message ID.
    #[inline]
    #[must_use]
    pub fn to_bytes(self) -> [u8; Self::LENGTH] {
        u64::to_le_bytes(self.0)
    }
}
impl fmt::Display for MessageId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:016x}", self.0.to_be())
    }
}
impl fmt::Debug for MessageId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}
impl From<[u8; Self::LENGTH]> for MessageId {
    fn from(message_id: [u8; Self::LENGTH]) -> Self {
        Self(u64::from_le_bytes(message_id))
    }
}

/// Whether delivery receipts should be sent.
#[derive(Clone, Copy, Debug, Name)]
#[cfg_attr(test, derive(PartialEq, Default))]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum MessageOverridesDeliveryReceiptPolicy {
    /// Apply the default behaviour as defined by the message's properties.
    #[cfg_attr(test, default)]
    Default,

    /// Don't send delivery receipts in response to the referred message.
    ///
    /// This overrides the default behaviour as defined by the message's properties.
    DontSend,
}

/// Message behaviour overrides that need to be stored.
#[derive(Clone, Copy, Name)]
#[cfg_attr(test, derive(PartialEq, Default))]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct MessageOverrides {
    /// Delivery receipt override policy.
    pub delivery_receipt_policy: MessageOverridesDeliveryReceiptPolicy,
}
impl Apply<MessageOverrides> for MessageFlags {
    fn apply(&mut self, overrides: MessageOverrides) {
        if matches!(
            overrides.delivery_receipt_policy,
            MessageOverridesDeliveryReceiptPolicy::DontSend
        ) {
            self.0 |= Self::NO_DELIVERY_RECEIPTS;
        }
    }
}
impl fmt::Debug for MessageOverrides {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Keep this format in sync with `MessageFlags`!
        write!(
            formatter,
            "{}({})",
            Self::NAME,
            itertools::join(
                [matches!(
                    self.delivery_receipt_policy,
                    MessageOverridesDeliveryReceiptPolicy::DontSend
                )
                .then_some("no-receipts"),]
                .into_iter()
                .flatten(),
                ", ",
            ),
        )
    }
}
impl From<MessageFlags> for MessageOverrides {
    fn from(flags: MessageFlags) -> Self {
        Self {
            delivery_receipt_policy: if flags.0 & MessageFlags::NO_DELIVERY_RECEIPTS != 0 {
                MessageOverridesDeliveryReceiptPolicy::DontSend
            } else {
                MessageOverridesDeliveryReceiptPolicy::Default
            },
        }
    }
}

/// Message flags which were/are transmitted to the server.
#[derive(Clone, Copy, Default, Name, PartialEq, Eq)]
pub struct MessageFlags(pub u8);
#[rustfmt::skip]
impl MessageFlags {
    pub(crate) const LENGTH: usize = 1;

    /// Whether a push should be sent by the chat server. Only meaningful for an outgoing message.
    pub const SEND_PUSH_NOTIFICATION:u8 =          0b_0000_0001;

    /// Whether the chat server should discard the message in case the receiver is not currently connected to
    /// the chat server. Only meaningful for an outgoing message.
    pub const NO_SERVER_QUEUING: u8 =              0b_0000_0010;

    /// Whether the message should not be acknowledged by the chat server (outgoing) or by the client
    /// (incoming).
    pub const NO_SERVER_ACKNOWLEDGEMENT: u8 =      0b_0000_0100;

    // Reserved:                                   0b_0000_1000
    // Reserved (formerly _group message marker_): 0b_0001_0000

    /// Instructs the chat server to only queue the message for a short period of time (currently 60 seconds).
    /// Only meaningful for an outgoing message.
    pub const SHORT_LIVED_SERVER_QUEUING: u8 =     0b_0010_0000;

    // Reserved:                                   0b_0100_0000

    /// If present, overrides behaviour of messages that would normally trigger a delivery receipt of type
    /// _received_ or _read_.
    pub const NO_DELIVERY_RECEIPTS: u8 =           0b_1000_0000;
}
impl fmt::Debug for MessageFlags {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let check_flag = |flag: u8, name: &'static str| -> Option<&'static str> {
            if self.0 & flag != 0 { Some(name) } else { None }
        };

        // Keep this format in sync with `MessageOverrides`!
        write!(
            formatter,
            "{}({})",
            Self::NAME,
            itertools::join(
                [
                    check_flag(Self::SEND_PUSH_NOTIFICATION, "push"),
                    check_flag(Self::NO_SERVER_QUEUING, "no-queue"),
                    check_flag(Self::NO_SERVER_ACKNOWLEDGEMENT, "no-ack"),
                    check_flag(Self::SHORT_LIVED_SERVER_QUEUING, "short-lived"),
                    check_flag(Self::NO_DELIVERY_RECEIPTS, "no-receipts"),
                ]
                .into_iter()
                .flatten(),
                ", ",
            ),
        )
    }
}

/// Metadata associated to a message.
#[derive(Debug, Clone)]
pub struct MessageMetadata {
    /// Unique message ID. Must match the message ID of the outer struct.
    pub message_id: MessageId,

    /// Unix-ish timestamp in milliseconds for when the message has been created.
    pub created_at: u64,

    /// Nickname of the sender at the time the message had been created.
    ///
    /// Note: If not present, this indicates that the nickname **did not change**.
    pub nickname: Option<String>,
}
impl MessageMetadata {
    /// Create for a new outgoing message.
    #[must_use]
    pub fn new_outgoing(message_id: MessageId, nickname: Option<String>) -> Self {
        Self {
            message_id,
            created_at: utc_now_ms(),
            nickname,
        }
    }
}
impl From<protobuf::csp_e2e::MessageMetadata> for MessageMetadata {
    fn from(metadata: protobuf::csp_e2e::MessageMetadata) -> Self {
        Self {
            message_id: MessageId(metadata.message_id),
            created_at: metadata.created_at,
            nickname: metadata.nickname.map(|nickname| nickname.trim().to_owned()),
        }
    }
}
impl From<MessageMetadata> for Vec<u8> {
    fn from(metadata: MessageMetadata) -> Self {
        let metadata = protobuf::csp_e2e::MessageMetadata {
            #[expect(deprecated, reason = "Will be filled by encode_to_vec_padded")]
            padding: vec![],
            message_id: metadata.message_id.0,
            created_at: metadata.created_at,
            nickname: metadata.nickname,
        };
        metadata.encode_to_vec_padded()
    }
}

/// An offset within a stream.
///
/// Note: The offset is relative to the *beginning* of the stream including any metadata, i.e. offset `0`
/// marks the first byte of the stream.
#[derive(Copy, Clone, Debug, Eq, Hash, Name, PartialEq, PartialOrd)]
#[cfg_attr(feature = "uniffi", derive(UniffiNewType))]
pub struct StreamOffset(pub u64);
impl fmt::Display for StreamOffset {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

Messung
CHZG
869810094

¤ Dauer der Verarbeitung: 0.12 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.