Skip to main content

core_crypto/mls/session/
id.rs

1use std::{
2    borrow::{Borrow, Cow},
3    fmt,
4    ops::Deref,
5};
6
7use uuid::Uuid;
8use wire_e2e_identity::E2eiClientId;
9
10use super::error::{Error, Result};
11use crate::HISTORY_CLIENT_ID_PREFIX;
12
13/// A Client identifier
14///
15/// A unique identifier for clients. A client is an identifier for each App a user is using, such as desktop,
16/// mobile, etc. Users can have multiple clients.
17/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-group-members-and-clients)
18#[derive(
19    core_crypto_macros::Debug, Clone, Eq, PartialOrd, Ord, Hash, derive_more::Into, serde::Serialize, serde::Deserialize,
20)]
21#[sensitive]
22pub struct ClientId(Vec<u8>);
23
24pub struct DeserializedClientId {
25    pub user_id: Uuid,
26    pub device_id: u64,
27    pub domain: String,
28}
29
30impl ClientId {
31    /// user-id & device-id separator
32    pub const DELIMITER: &'static str = ":";
33    /// seperator between a user's device and domain
34    pub const DOMAIN_SEPERATOR: &'static str = "@";
35
36    /// Create a new client ID.
37    pub fn new(user_id: Uuid, device_id: u64, domain: &str) -> Self {
38        let string = format!(
39            "{user_id}{delimiter}{device_id:x}{seperator}{domain}",
40            user_id = user_id.hyphenated(),
41            delimiter = Self::DELIMITER,
42            seperator = Self::DOMAIN_SEPERATOR
43        );
44        let bytes = string.into_bytes();
45        #[cfg(debug_assertions)]
46        Self::try_parse_bytes(&bytes).expect("client id format string is correct");
47        Self(bytes)
48    }
49
50    /// In some cases, we still have a base64-encoded user id, e.g., certificates.
51    pub(crate) fn try_from_str_with_base64_user_id(str_with_base64_user_id: &str) -> Result<Self> {
52        E2eiClientId::try_from_qualified(str_with_base64_user_id)
53            .map(ClientId::from_e2ei_client_id)
54            .map_err(|_| Error::InvalidQualifiedClientId)
55    }
56
57    fn from_e2ei_client_id(e2ei_client_id: E2eiClientId) -> Self {
58        Self::new(e2ei_client_id.user_id, e2ei_client_id.device_id, &e2ei_client_id.domain)
59    }
60
61    pub(crate) fn new_ephemeral() -> Self {
62        let user_id = Uuid::new_v4().hyphenated().to_string();
63        let bytes = format!("{HISTORY_CLIENT_ID_PREFIX}{}{user_id}", Self::DELIMITER).into_bytes();
64        Self(bytes)
65    }
66
67    /// Deserialize the client ID into its parts
68    pub fn deserialize(&self) -> DeserializedClientId {
69        let (user_id, device_id, domain) = Self::try_parse_bytes(&self.0).expect("We just invert initialization");
70
71        DeserializedClientId {
72            user_id,
73            device_id,
74            domain,
75        }
76    }
77
78    pub(crate) fn new_from_bytes(bytes: Vec<u8>) -> Result<Self> {
79        Self::try_parse_bytes(&bytes)?;
80        Ok(Self(bytes))
81    }
82
83    /// Deconstruct the format string created during intialization
84    fn try_parse_bytes(bytes: &[u8]) -> Result<(Uuid, u64, String)> {
85        let client_id = std::str::from_utf8(bytes).map_err(|_| Error::InvalidQualifiedClientId)?;
86        let (user_id, rest) = client_id
87            .split_once(Self::DELIMITER)
88            .ok_or(Error::InvalidQualifiedClientId)?;
89        let user_id = Self::parse_user_id(user_id)?;
90        let (device_id, domain) = rest
91            .split_once(Self::DOMAIN_SEPERATOR)
92            .ok_or(Error::InvalidQualifiedClientId)?;
93        let device_id = Self::parse_device_id(device_id)?;
94        Ok((user_id, device_id, domain.to_owned()))
95    }
96
97    /// Parse the user id, assuming string representation of a UUIDv4.
98    fn parse_user_id(user_id: &str) -> Result<Uuid> {
99        Uuid::try_parse(user_id).map_err(|_| Error::InvalidQualifiedClientId)
100    }
101
102    fn parse_device_id(device_id: &str) -> Result<u64> {
103        u64::from_str_radix(device_id, 16).map_err(|_| Error::InvalidQualifiedClientId)
104    }
105
106    #[cfg(test)]
107    pub(crate) fn as_bytes(&self) -> &[u8] {
108        &self.0
109    }
110
111    /// Parse this into the representation required by the e2ei crate.
112    pub fn as_e2ei_client_id(&self) -> E2eiClientId {
113        let (user_id, device_id, domain) = Self::try_parse_bytes(&self.0).expect("We just invert initialization");
114        E2eiClientId::try_new(user_id.to_string(), device_id, &domain).expect("We just invert intialization")
115    }
116
117    pub(crate) fn into_inner(self) -> Vec<u8> {
118        self.0
119    }
120}
121
122impl TryFrom<&[u8]> for ClientId {
123    type Error = Error;
124
125    fn try_from(value: &[u8]) -> Result<Self> {
126        Self::try_parse_bytes(value)?;
127        Ok(Self(value.into()))
128    }
129}
130
131impl Deref for ClientId {
132    type Target = ClientIdRef;
133
134    fn deref(&self) -> &Self::Target {
135        ClientIdRef::new(&self.0)
136    }
137}
138
139impl AsRef<[u8]> for ClientId {
140    fn as_ref(&self) -> &[u8] {
141        &self.0
142    }
143}
144
145impl AsRef<ClientIdRef> for ClientId {
146    fn as_ref(&self) -> &ClientIdRef {
147        ClientIdRef::new(&self.0)
148    }
149}
150
151impl std::fmt::Display for ClientId {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", hex::encode(self.0.as_slice()))
154    }
155}
156
157impl<T> PartialEq<T> for ClientId
158where
159    ClientIdRef: PartialEq<T>,
160{
161    fn eq(&self, other: &T) -> bool {
162        (**self).eq(other)
163    }
164}
165
166// Needed for proteus tests
167#[cfg(test)]
168impl From<&str> for ClientId {
169    fn from(value: &str) -> Self {
170        Self(value.as_bytes().into())
171    }
172}
173
174/// Reference to a [`ClientId`].
175///
176/// This type is `!Sized` and is only ever seen as a reference, like `str` or `[u8]`.
177//
178// pattern from https://stackoverflow.com/a/64990850
179#[repr(transparent)]
180#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Deref)]
181pub struct ClientIdRef([u8]);
182
183impl ClientIdRef {
184    /// Creates a `ClientId` Ref, needed to implement `Borrow<ClientIdRef>` for `T`
185    pub fn new<Bytes>(bytes: &Bytes) -> &ClientIdRef
186    where
187        Bytes: AsRef<[u8]> + ?Sized,
188    {
189        // safety: because of `repr(transparent)` we know that `ClientIdRef` has a memory layout
190        // identical to `[u8]`, so we can perform this cast
191        unsafe { &*(bytes.as_ref() as *const [u8] as *const ClientIdRef) }
192    }
193
194    /// View this reference as a byte slice
195    pub fn as_slice(&self) -> &[u8] {
196        self.as_ref()
197    }
198}
199
200impl<'a> From<&'a [u8]> for &'a ClientIdRef {
201    fn from(value: &'a [u8]) -> Self {
202        ClientIdRef::new(value)
203    }
204}
205
206impl<'a> From<&'a Vec<u8>> for &'a ClientIdRef {
207    fn from(value: &'a Vec<u8>) -> Self {
208        ClientIdRef::new(value.as_slice())
209    }
210}
211
212impl Borrow<ClientIdRef> for ClientId {
213    fn borrow(&self) -> &ClientIdRef {
214        ClientIdRef::new(&self.0)
215    }
216}
217
218impl Borrow<ClientIdRef> for &'_ ClientId {
219    fn borrow(&self) -> &ClientIdRef {
220        ClientIdRef::new(&*self.0)
221    }
222}
223
224impl ToOwned for ClientIdRef {
225    type Owned = ClientId;
226
227    fn to_owned(&self) -> Self::Owned {
228        ClientId(self.0.to_owned())
229    }
230}
231
232impl AsRef<[u8]> for ClientIdRef {
233    fn as_ref(&self) -> &[u8] {
234        &self.0
235    }
236}
237
238impl<'a> From<&'a ClientIdRef> for Cow<'a, [u8]> {
239    fn from(value: &'a ClientIdRef) -> Self {
240        Cow::Borrowed(value.as_ref())
241    }
242}
243
244impl PartialEq<ClientId> for ClientIdRef {
245    fn eq(&self, other: &ClientId) -> bool {
246        &self.0 == other.as_slice()
247    }
248}
249
250impl PartialEq<[u8]> for ClientIdRef {
251    fn eq(&self, other: &[u8]) -> bool {
252        &self.0 == other
253    }
254}
255
256impl PartialEq<&'_ [u8]> for ClientIdRef {
257    fn eq(&self, other: &&'_ [u8]) -> bool {
258        &self.0 == *other
259    }
260}
261
262impl std::fmt::Display for ClientIdRef {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(f, "{}", hex::encode(&self.0))
265    }
266}
267
268macro_rules! impl_eq {
269    ($( $t:ty => |$self:ident, $other:ident| $impl:expr ; )+) => {
270        $(
271            impl PartialEq<$t> for ClientIdRef {
272                fn eq(&self, other: &$t) -> bool {
273                    let $self = self;
274                    let $other = other;
275                    $impl
276                }
277            }
278
279            impl PartialEq<ClientIdRef> for $t {
280                fn eq(&self, other: &ClientIdRef) -> bool {
281                    other.eq(self)
282                }
283            }
284
285            impl PartialEq<$t> for &'_ ClientIdRef {
286                fn eq(&self, other: &$t) -> bool {
287                    let $self = self;
288                    let $other = other;
289                    $impl
290                }
291            }
292
293            impl PartialEq<&'_ ClientIdRef> for $t {
294                fn eq(&self, other: &&'_ ClientIdRef) -> bool {
295                    other.eq(self)
296                }
297            }
298        )+
299    };
300}
301
302impl_eq!(
303    Vec<u8> => |me, other| me.0.eq(other.as_slice());
304    Cow<'_, ClientIdRef> => |me, other| me.eq(&other.as_slice());
305);
306
307// we can't use `core_crypto_macros::Debug` to generate this because `ClientIdRef: !Sized`,
308// and the `log` crate maintainers did not explicitly opt-in to allowing `!Sized` in their
309// `Value::from_debug` impl, even though it might make sense to.
310//
311// this has the consequence that we can't natively log a `ClientIdRef` as a value;
312// if we want to, we have to do `id_ref.to_owned()`. Which might be ok.
313impl fmt::Debug for ClientIdRef {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        f.debug_tuple("ClientIdRef")
316            .field(&obfuscate::Obfuscated::from(&self.0))
317            .finish()
318    }
319}
320
321type LegacyClientId = wire_e2e_identity::legacy::id::ClientId;
322
323impl From<ClientId> for LegacyClientId {
324    fn from(value: ClientId) -> Self {
325        Self::from(value.0)
326    }
327}
328#[cfg(test)]
329impl ClientId {
330    pub(crate) fn as_user_id(&self) -> Uuid {
331        self.deserialize().user_id
332    }
333
334    pub(crate) fn with_user(&self) -> (ClientId, Uuid) {
335        (self.clone(), self.as_user_id())
336    }
337}