libsignal_service/websocket/
usernames.rs

1use crate::utils::serde_base64;
2use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
3use libsignal_core::{Aci, ServiceIdKind};
4use reqwest::Method;
5
6use crate::content::ServiceError;
7
8use super::{SignalWebSocket, Unidentified};
9
10impl SignalWebSocket<Unidentified> {
11    pub async fn look_up_username(
12        &mut self,
13        username: &usernames::Username,
14    ) -> Result<Option<Aci>, ServiceError> {
15        self.look_up_username_hash(&username.hash()).await
16    }
17
18    // Based on libsignal-net
19    pub async fn look_up_username_hash(
20        &mut self,
21        hash: &[u8],
22    ) -> Result<Option<Aci>, ServiceError> {
23        #[derive(serde::Deserialize)]
24        struct UsernameHashResponse {
25            uuid: String,
26        }
27
28        let response = self
29            .http_request(
30                Method::GET,
31                format!(
32                    "/v1/accounts/username_hash/{}",
33                    BASE64_URL_SAFE_NO_PAD.encode(hash)
34                ),
35            )?
36            .send()
37            .await?;
38
39        if response.status() == 404 {
40            tracing::debug!("username not found");
41            return Ok(None);
42        }
43
44        let result: UsernameHashResponse =
45            response.service_error_for_status().await?.json().await?;
46
47        Ok(Some(
48            Aci::parse_from_service_id_string(&result.uuid).ok_or_else(
49                || ServiceError::InvalidAddressType(ServiceIdKind::Aci),
50            )?,
51        ))
52    }
53
54    // Based on libsignal-net
55    pub async fn look_up_username_link(
56        &mut self,
57        uuid: uuid::Uuid,
58        entropy: &[u8; usernames::constants::USERNAME_LINK_ENTROPY_SIZE],
59    ) -> Result<Option<usernames::Username>, ServiceError> {
60        #[derive(serde::Deserialize)]
61        struct UsernameLinkResponse {
62            #[serde(rename = "usernameLinkEncryptedValue")]
63            #[serde(with = "serde_base64")]
64            encrypted_username: Vec<u8>,
65        }
66
67        let response = self
68            .http_request(
69                Method::GET,
70                format!("/v1/accounts/username_link/{uuid}",),
71            )?
72            .send()
73            .await?;
74
75        if response.status() == 404 {
76            tracing::debug!("username link not found");
77            return Ok(None);
78        }
79
80        let result: UsernameLinkResponse =
81            response.service_error_for_status().await?.json().await?;
82
83        let plaintext_username =
84            usernames::decrypt_username(entropy, &result.encrypted_username)
85                .map_err(|_e| {
86                    tracing::error!(error=%_e, "undecryptable username");
87                    ServiceError::InvalidFrame {
88                        reason: "undecryptable username link",
89                    }
90                })?;
91
92        let validated_username = usernames::Username::new(&plaintext_username).map_err(|e| {
93            // Exhaustively match UsernameError to make sure there's nothing we shouldn't log.
94            #[allow(clippy::let_unit_value)]
95            let _username_error_carries_no_information_that_would_be_bad_to_log = match e {
96                usernames::UsernameError::MissingSeparator
97                | usernames::UsernameError::NicknameCannotBeEmpty
98                | usernames::UsernameError::NicknameCannotStartWithDigit
99                | usernames::UsernameError::BadNicknameCharacter
100                | usernames::UsernameError::NicknameTooShort
101                | usernames::UsernameError::NicknameTooLong
102                | usernames::UsernameError::DiscriminatorCannotBeEmpty
103                | usernames::UsernameError::DiscriminatorCannotBeZero
104                | usernames::UsernameError::DiscriminatorCannotBeSingleDigit
105                | usernames::UsernameError::DiscriminatorCannotHaveLeadingZeros
106                | usernames::UsernameError::BadDiscriminatorCharacter
107                | usernames::UsernameError::DiscriminatorTooLarge => {}
108            };
109            tracing::warn!(error=%e, "username link decrypted to an invalid username");
110            tracing::debug!(error=%e,
111                "username link decrypted to '{plaintext_username}', which is not valid"
112            );
113            // The user didn't ever type this username, so the precise way in which it's invalid
114            // isn't important. Treat this equivalent to having found garbage data in the link. This
115            // simplifies error handling for callers.
116            ServiceError::InvalidFrame {
117                reason: "undecryptable username link",
118            }
119        })?;
120
121        Ok(Some(validated_username))
122    }
123}