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