libsignal_service/websocket/
usernames.rs1use 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 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 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 #[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 ServiceError::InvalidFrame {
117 reason: "undecryptable username link",
118 }
119 })?;
120
121 Ok(Some(validated_username))
122 }
123}