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    /// Encode a device id the way it appears within a client id: unpadded lowercase hex.
36    ///
37    /// This is the single definition of that encoding, so that anything rendering a device id as
38    /// part of a client id reproduces the bytes of the [`ClientId`] it came from.
39    ///
40    /// It must stay unpadded. `rusty_jwt_tools` uses the same encoding for the `wireapp://` SAN of
41    /// an E2EI certificate, and that URI is what `e2e-identity` sends to the ACME server, so the
42    /// certificates already issued in the field carry this form. For an X509 credential the client
43    /// id *is* the certificate's identity, so widening it here would put us out of step with them.
44    //
45    // Parsing is more lenient than this: [`Self::new_from_bytes`] accepts a zero-padded device id,
46    // since other implementations may produce one.
47    pub fn encode_device_id(device_id: u64) -> String {
48        format!("{device_id:x}")
49    }
50
51    /// Create a new client ID.
52    pub fn new(user_id: Uuid, device_id: u64, domain: &str) -> Self {
53        let string = format!(
54            "{user_id}{delimiter}{device_id}{seperator}{domain}",
55            user_id = user_id.hyphenated(),
56            delimiter = Self::DELIMITER,
57            device_id = Self::encode_device_id(device_id),
58            seperator = Self::DOMAIN_SEPERATOR
59        );
60        let bytes = string.into_bytes();
61        #[cfg(debug_assertions)]
62        Self::try_parse_bytes(&bytes).expect("client id format string is correct");
63        Self(bytes)
64    }
65
66    /// In some cases, we still have a base64-encoded user id, e.g., certificates.
67    pub(crate) fn try_from_str_with_base64_user_id(str_with_base64_user_id: &str) -> Result<Self> {
68        E2eiClientId::try_from_qualified(str_with_base64_user_id)
69            .map(ClientId::from_e2ei_client_id)
70            .map_err(|_| Error::InvalidQualifiedClientId)
71    }
72
73    fn from_e2ei_client_id(e2ei_client_id: E2eiClientId) -> Self {
74        Self::new(e2ei_client_id.user_id, e2ei_client_id.device_id, &e2ei_client_id.domain)
75    }
76
77    pub(crate) fn new_ephemeral() -> Self {
78        let user_id = Uuid::new_v4().hyphenated().to_string();
79        let bytes = format!("{HISTORY_CLIENT_ID_PREFIX}{}{user_id}", Self::DELIMITER).into_bytes();
80        Self(bytes)
81    }
82
83    /// Deserialize the client ID into its parts
84    pub fn deserialize(&self) -> DeserializedClientId {
85        let (user_id, device_id, domain) = Self::try_parse_bytes(&self.0).expect("We just invert initialization");
86
87        DeserializedClientId {
88            user_id,
89            device_id,
90            domain,
91        }
92    }
93
94    pub(crate) fn new_from_bytes(bytes: Vec<u8>) -> Result<Self> {
95        Self::try_parse_bytes(&bytes)?;
96        Ok(Self(bytes))
97    }
98
99    /// Deconstruct the format string created during intialization
100    fn try_parse_bytes(bytes: &[u8]) -> Result<(Uuid, u64, String)> {
101        let client_id = std::str::from_utf8(bytes).map_err(|_| Error::InvalidQualifiedClientId)?;
102        let (user_id, rest) = client_id
103            .split_once(Self::DELIMITER)
104            .ok_or(Error::InvalidQualifiedClientId)?;
105        let user_id = Self::parse_user_id(user_id)?;
106        let (device_id, domain) = rest
107            .split_once(Self::DOMAIN_SEPERATOR)
108            .ok_or(Error::InvalidQualifiedClientId)?;
109        let device_id = Self::parse_device_id(device_id)?;
110        Ok((user_id, device_id, domain.to_owned()))
111    }
112
113    /// Parse the user id, assuming string representation of a UUIDv4.
114    fn parse_user_id(user_id: &str) -> Result<Uuid> {
115        Uuid::try_parse(user_id).map_err(|_| Error::InvalidQualifiedClientId)
116    }
117
118    fn parse_device_id(device_id: &str) -> Result<u64> {
119        u64::from_str_radix(device_id, 16).map_err(|_| Error::InvalidQualifiedClientId)
120    }
121
122    #[cfg(test)]
123    pub(crate) fn as_bytes(&self) -> &[u8] {
124        &self.0
125    }
126
127    /// Parse this into the representation required by the e2ei crate.
128    pub fn as_e2ei_client_id(&self) -> E2eiClientId {
129        let (user_id, device_id, domain) = Self::try_parse_bytes(&self.0).expect("We just invert initialization");
130        E2eiClientId::try_new(user_id.to_string(), device_id, &domain).expect("We just invert intialization")
131    }
132
133    pub(crate) fn into_inner(self) -> Vec<u8> {
134        self.0
135    }
136}
137
138impl TryFrom<&[u8]> for ClientId {
139    type Error = Error;
140
141    fn try_from(value: &[u8]) -> Result<Self> {
142        Self::try_parse_bytes(value)?;
143        Ok(Self(value.into()))
144    }
145}
146
147impl Deref for ClientId {
148    type Target = ClientIdRef;
149
150    fn deref(&self) -> &Self::Target {
151        ClientIdRef::new(&self.0)
152    }
153}
154
155impl AsRef<[u8]> for ClientId {
156    fn as_ref(&self) -> &[u8] {
157        &self.0
158    }
159}
160
161impl AsRef<ClientIdRef> for ClientId {
162    fn as_ref(&self) -> &ClientIdRef {
163        ClientIdRef::new(&self.0)
164    }
165}
166
167impl std::fmt::Display for ClientId {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(f, "{}", hex::encode(self.0.as_slice()))
170    }
171}
172
173impl<T> PartialEq<T> for ClientId
174where
175    ClientIdRef: PartialEq<T>,
176{
177    fn eq(&self, other: &T) -> bool {
178        (**self).eq(other)
179    }
180}
181
182// Needed for proteus tests
183#[cfg(test)]
184impl From<&str> for ClientId {
185    fn from(value: &str) -> Self {
186        Self(value.as_bytes().into())
187    }
188}
189
190/// Reference to a [`ClientId`].
191///
192/// This type is `!Sized` and is only ever seen as a reference, like `str` or `[u8]`.
193//
194// pattern from https://stackoverflow.com/a/64990850
195#[repr(transparent)]
196#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Deref)]
197pub struct ClientIdRef([u8]);
198
199impl ClientIdRef {
200    /// Creates a `ClientId` Ref, needed to implement `Borrow<ClientIdRef>` for `T`
201    pub fn new<Bytes>(bytes: &Bytes) -> &ClientIdRef
202    where
203        Bytes: AsRef<[u8]> + ?Sized,
204    {
205        // safety: because of `repr(transparent)` we know that `ClientIdRef` has a memory layout
206        // identical to `[u8]`, so we can perform this cast
207        unsafe { &*(bytes.as_ref() as *const [u8] as *const ClientIdRef) }
208    }
209
210    /// View this reference as a byte slice
211    pub fn as_slice(&self) -> &[u8] {
212        self.as_ref()
213    }
214}
215
216impl<'a> From<&'a [u8]> for &'a ClientIdRef {
217    fn from(value: &'a [u8]) -> Self {
218        ClientIdRef::new(value)
219    }
220}
221
222impl<'a> From<&'a Vec<u8>> for &'a ClientIdRef {
223    fn from(value: &'a Vec<u8>) -> Self {
224        ClientIdRef::new(value.as_slice())
225    }
226}
227
228impl Borrow<ClientIdRef> for ClientId {
229    fn borrow(&self) -> &ClientIdRef {
230        ClientIdRef::new(&self.0)
231    }
232}
233
234impl Borrow<ClientIdRef> for &'_ ClientId {
235    fn borrow(&self) -> &ClientIdRef {
236        ClientIdRef::new(&*self.0)
237    }
238}
239
240impl ToOwned for ClientIdRef {
241    type Owned = ClientId;
242
243    fn to_owned(&self) -> Self::Owned {
244        ClientId(self.0.to_owned())
245    }
246}
247
248impl AsRef<[u8]> for ClientIdRef {
249    fn as_ref(&self) -> &[u8] {
250        &self.0
251    }
252}
253
254impl<'a> From<&'a ClientIdRef> for Cow<'a, [u8]> {
255    fn from(value: &'a ClientIdRef) -> Self {
256        Cow::Borrowed(value.as_ref())
257    }
258}
259
260impl PartialEq<ClientId> for ClientIdRef {
261    fn eq(&self, other: &ClientId) -> bool {
262        &self.0 == other.as_slice()
263    }
264}
265
266impl PartialEq<[u8]> for ClientIdRef {
267    fn eq(&self, other: &[u8]) -> bool {
268        &self.0 == other
269    }
270}
271
272impl PartialEq<&'_ [u8]> for ClientIdRef {
273    fn eq(&self, other: &&'_ [u8]) -> bool {
274        &self.0 == *other
275    }
276}
277
278impl std::fmt::Display for ClientIdRef {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        write!(f, "{}", hex::encode(&self.0))
281    }
282}
283
284macro_rules! impl_eq {
285    ($( $t:ty => |$self:ident, $other:ident| $impl:expr ; )+) => {
286        $(
287            impl PartialEq<$t> for ClientIdRef {
288                fn eq(&self, other: &$t) -> bool {
289                    let $self = self;
290                    let $other = other;
291                    $impl
292                }
293            }
294
295            impl PartialEq<ClientIdRef> for $t {
296                fn eq(&self, other: &ClientIdRef) -> bool {
297                    other.eq(self)
298                }
299            }
300
301            impl PartialEq<$t> for &'_ ClientIdRef {
302                fn eq(&self, other: &$t) -> bool {
303                    let $self = self;
304                    let $other = other;
305                    $impl
306                }
307            }
308
309            impl PartialEq<&'_ ClientIdRef> for $t {
310                fn eq(&self, other: &&'_ ClientIdRef) -> bool {
311                    other.eq(self)
312                }
313            }
314        )+
315    };
316}
317
318impl_eq!(
319    Vec<u8> => |me, other| me.0.eq(other.as_slice());
320    Cow<'_, ClientIdRef> => |me, other| me.eq(&other.as_slice());
321);
322
323// we can't use `core_crypto_macros::Debug` to generate this because `ClientIdRef: !Sized`,
324// and the `log` crate maintainers did not explicitly opt-in to allowing `!Sized` in their
325// `Value::from_debug` impl, even though it might make sense to.
326//
327// this has the consequence that we can't natively log a `ClientIdRef` as a value;
328// if we want to, we have to do `id_ref.to_owned()`. Which might be ok.
329impl fmt::Debug for ClientIdRef {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        f.debug_tuple("ClientIdRef")
332            .field(&obfuscate::Obfuscated::from(&self.0))
333            .finish()
334    }
335}
336
337#[cfg(test)]
338impl ClientId {
339    pub(crate) fn as_user_id(&self) -> Uuid {
340        self.deserialize().user_id
341    }
342
343    pub(crate) fn with_user(&self) -> (ClientId, Uuid) {
344        (self.clone(), self.as_user_id())
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use uuid::Uuid;
351
352    use super::ClientId;
353
354    const USER_ID: Uuid = Uuid::from_u128(0x0195_7b1e_4f00_7c9b_8a3d_2e11_9c44_0d7f);
355    const DOMAIN: &str = "wire.com";
356
357    /// The device id inside a client id is unpadded lowercase hex.
358    ///
359    /// This has to stay in step with `rusty_jwt_tools`' `hex_encoded_device_id`, which is what
360    /// encodes the device id into the `wireapp://` SAN of an E2EI certificate (see
361    /// `e2e-identity`'s ACME identifier, which sends `to_uri()` to the ACME server). For an X509
362    /// credential the client id *is* the certificate's identity, so padding here would put us out
363    /// of step with every certificate already issued.
364    #[test]
365    fn device_id_is_encoded_as_unpadded_hex() {
366        for (device_id, expected) in [
367            (0x0, "0"),
368            (0xf, "f"),
369            (0x8e64_2443_0d3b_28be, "8e6424430d3b28be"),
370            (u64::MAX, "ffffffffffffffff"),
371        ] {
372            assert_eq!(ClientId::encode_device_id(device_id), expected);
373
374            let client_id = ClientId::new(USER_ID, device_id, DOMAIN);
375            assert_eq!(
376                std::str::from_utf8(client_id.as_bytes()).unwrap(),
377                format!("{}:{expected}@{DOMAIN}", USER_ID.hyphenated()),
378            );
379        }
380    }
381
382    /// Whatever we render a client id as has to parse back to the same field values.
383    #[test]
384    fn client_id_round_trips_through_its_own_encoding() {
385        for device_id in [0x0, 0xf, 0x00ff, 0x8e64_2443_0d3b_28be, u64::MAX] {
386            let client_id = ClientId::new(USER_ID, device_id, DOMAIN);
387            let deserialized = client_id.deserialize();
388
389            assert_eq!(deserialized.user_id, USER_ID);
390            assert_eq!(deserialized.device_id, device_id);
391            assert_eq!(deserialized.domain, DOMAIN);
392        }
393    }
394
395    /// Parsing is lenient about padding even though we never produce it, so a zero-padded id from
396    /// another implementation still resolves to the right device id. Note that it does not produce
397    /// a byte-equal `ClientId`, because parsing preserves the input bytes.
398    #[test]
399    fn a_padded_device_id_still_parses() {
400        let padded = format!("{}:000000000000000f@{DOMAIN}", USER_ID.hyphenated());
401        let client_id =
402            ClientId::new_from_bytes(padded.clone().into_bytes()).expect("a padded device id must still parse");
403
404        assert_eq!(std::str::from_utf8(client_id.as_bytes()).unwrap(), padded);
405        assert_eq!(client_id.deserialize().device_id, 0xf);
406        assert_ne!(client_id, ClientId::new(USER_ID, 0xf, DOMAIN));
407    }
408}