libsignal_service/websocket/
registration.rs

1use libsignal_protocol::IdentityKey;
2use reqwest::Method;
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use super::ServiceError;
7use crate::{
8    pre_keys::{KyberPreKeyEntity, SignedPreKeyEntity},
9    utils::serde_base64,
10    websocket::{self, account::AccountAttributes, SignalWebSocket},
11};
12
13/// This type is used in registration lock handling.
14/// It's identical with HttpAuth, but used to avoid type confusion.
15#[derive(derive_more::Debug, Clone, Serialize, Deserialize)]
16pub struct AuthCredentials {
17    pub username: String,
18    #[debug(ignore)]
19    pub password: String,
20}
21
22#[derive(Debug, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct RegistrationLockFailure {
25    pub length: Option<u32>,
26    pub time_remaining: Option<u64>,
27    #[serde(rename = "backup_credentials")]
28    pub svr1_credentials: Option<AuthCredentials>,
29    pub svr2_credentials: Option<AuthCredentials>,
30}
31
32#[derive(Debug, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct VerifyAccountResponse {
35    #[serde(rename = "uuid")]
36    pub aci: Uuid,
37    pub pni: Uuid,
38    pub storage_capable: bool,
39    #[serde(default)]
40    pub number: Option<String>,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum VerificationTransport {
46    Sms,
47    Voice,
48}
49
50#[derive(Clone, Debug)]
51pub enum RegistrationMethod<'a> {
52    SessionId(&'a str),
53    RecoveryPassword(&'a str),
54}
55
56impl<'a> RegistrationMethod<'a> {
57    pub fn session_id(&'a self) -> Option<&'a str> {
58        match self {
59            Self::SessionId(x) => Some(x),
60            _ => None,
61        }
62    }
63
64    pub fn recovery_password(&'a self) -> Option<&'a str> {
65        match self {
66            Self::RecoveryPassword(x) => Some(x),
67            _ => None,
68        }
69    }
70}
71
72#[derive(Debug, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct DeviceActivationRequest {
75    pub aci_signed_pre_key: SignedPreKeyEntity,
76    pub pni_signed_pre_key: SignedPreKeyEntity,
77    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
78    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
79}
80
81#[derive(Debug, Serialize)]
82pub struct CaptchaAttributes<'a> {
83    #[serde(rename = "type")]
84    pub challenge_type: &'a str,
85    pub token: &'a str,
86    pub captcha: &'a str,
87}
88
89#[derive(Debug, Clone, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct RegistrationSessionMetadataResponse {
92    pub id: String,
93    #[serde(default)]
94    pub next_sms: Option<i32>,
95    #[serde(default)]
96    pub next_call: Option<i32>,
97    #[serde(default)]
98    pub next_verification_attempt: Option<i32>,
99    pub allowed_to_request_code: bool,
100    #[serde(default)]
101    pub requested_information: Vec<String>,
102    pub verified: bool,
103}
104
105impl RegistrationSessionMetadataResponse {
106    pub fn push_challenge_required(&self) -> bool {
107        // .contains() requires &String ...
108        self.requested_information
109            .iter()
110            .any(|x| x.as_str() == "pushChallenge")
111    }
112
113    pub fn captcha_required(&self) -> bool {
114        // .contains() requires &String ...
115        self.requested_information
116            .iter()
117            .any(|x| x.as_str() == "captcha")
118    }
119}
120
121impl SignalWebSocket<websocket::Unidentified> {
122    // Equivalent of Java's
123    // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc)
124    pub async fn create_verification_session<'a>(
125        &mut self,
126        number: &'a str,
127        push_token: Option<&'a str>,
128        mcc: Option<&'a str>,
129        mnc: Option<&'a str>,
130    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
131        #[derive(serde::Serialize, Debug)]
132        #[serde(rename_all = "camelCase")]
133        struct VerificationSessionMetadataRequestBody<'a> {
134            number: &'a str,
135            push_token: Option<&'a str>,
136            mcc: Option<&'a str>,
137            mnc: Option<&'a str>,
138            push_token_type: Option<&'a str>,
139        }
140
141        self.http_request(Method::POST, "/v1/verification/session")?
142            .send_json(&VerificationSessionMetadataRequestBody {
143                number,
144                push_token_type: push_token.as_ref().map(|_| "fcm"),
145                push_token,
146                mcc,
147                mnc,
148            })
149            .await?
150            .service_error_for_status()
151            .await?
152            .json()
153            .await
154    }
155
156    pub async fn patch_verification_session<'a>(
157        &mut self,
158        session_id: &'a str,
159        push_token: Option<&'a str>,
160        mcc: Option<&'a str>,
161        mnc: Option<&'a str>,
162        captcha: Option<&'a str>,
163        push_challenge: Option<&'a str>,
164    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
165        #[derive(serde::Serialize, Debug)]
166        #[serde(rename_all = "camelCase")]
167        struct UpdateVerificationSessionRequestBody<'a> {
168            captcha: Option<&'a str>,
169            push_token: Option<&'a str>,
170            push_challenge: Option<&'a str>,
171            mcc: Option<&'a str>,
172            mnc: Option<&'a str>,
173            push_token_type: Option<&'a str>,
174        }
175
176        self.http_request(
177            Method::PATCH,
178            format!("/v1/verification/session/{}", session_id),
179        )?
180        .send_json(&UpdateVerificationSessionRequestBody {
181            captcha,
182            push_token_type: push_token.as_ref().map(|_| "fcm"),
183            push_token,
184            mcc,
185            mnc,
186            push_challenge,
187        })
188        .await?
189        .service_error_for_status()
190        .await?
191        .json()
192        .await
193    }
194
195    // Equivalent of Java's
196    // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport)
197    /// Request a verification code.
198    ///
199    /// Signal requires a client type, and they use these three strings internally:
200    ///   - "android-2021-03"
201    ///   - "android"
202    ///   - "ios"
203    ///
204    /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In
205    /// principle, they will consider any string as "unknown", so other strings may work too.
206    pub async fn request_verification_code(
207        &mut self,
208        session_id: &str,
209        client: &str,
210        // XXX: We currently don't support this, because we need to set some headers in the
211        //      post_json() call
212        // locale: Option<String>,
213        transport: VerificationTransport,
214    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
215        #[derive(Debug, Serialize)]
216        struct VerificationCodeRequest<'a> {
217            transport: VerificationTransport,
218            client: &'a str,
219        }
220
221        self.http_request(
222            Method::POST,
223            format!("/v1/verification/session/{}/code", session_id),
224        )?
225        .send_json(&VerificationCodeRequest { transport, client })
226        .await?
227        .service_error_for_status()
228        .await?
229        .json()
230        .await
231    }
232}
233
234impl SignalWebSocket<websocket::Identified> {
235    pub async fn submit_registration_request(
236        &mut self,
237        registration_method: RegistrationMethod<'_>,
238        account_attributes: AccountAttributes,
239        skip_device_transfer: bool,
240        aci_identity_key: &IdentityKey,
241        pni_identity_key: &IdentityKey,
242        device_activation_request: DeviceActivationRequest,
243    ) -> Result<VerifyAccountResponse, ServiceError> {
244        #[derive(serde::Serialize, Debug)]
245        #[serde(rename_all = "camelCase")]
246        struct RegistrationSessionRequestBody<'a> {
247            // Unhandled response 422 with body:
248            // {"errors":["deviceActivationRequest.pniSignedPreKey must not be
249            // null","deviceActivationRequest.pniPqLastResortPreKey must not be
250            // null","everySignedKeyValid must be true","aciIdentityKey must not be
251            // null","pniIdentityKey must not be null","deviceActivationRequest.aciSignedPreKey
252            // must not be null","deviceActivationRequest.aciPqLastResortPreKey must not be null"]}
253            session_id: Option<&'a str>,
254            recovery_password: Option<&'a str>,
255            account_attributes: AccountAttributes,
256            skip_device_transfer: bool,
257            every_signed_key_valid: bool,
258            #[serde(default, with = "serde_base64")]
259            pni_identity_key: Vec<u8>,
260            #[serde(default, with = "serde_base64")]
261            aci_identity_key: Vec<u8>,
262            #[serde(flatten)]
263            device_activation_request: DeviceActivationRequest,
264        }
265
266        self.http_request(Method::POST, "/v1/registration")?
267            .send_json(&RegistrationSessionRequestBody {
268                session_id: registration_method.session_id(),
269                recovery_password: registration_method.recovery_password(),
270                account_attributes,
271                skip_device_transfer,
272                aci_identity_key: aci_identity_key.serialize().into(),
273                pni_identity_key: pni_identity_key.serialize().into(),
274                device_activation_request,
275                every_signed_key_valid: true,
276            })
277            .await?
278            .service_error_for_status()
279            .await?
280            .json()
281            .await
282    }
283
284    pub async fn submit_verification_code(
285        &mut self,
286        session_id: &str,
287        verification_code: &str,
288    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
289        #[derive(Debug, Serialize)]
290        struct VerificationCode<'a> {
291            code: &'a str,
292        }
293
294        self.http_request(
295            Method::PUT,
296            format!("/v1/verification/session/{}/code", session_id),
297        )?
298        .send_json(&VerificationCode {
299            code: verification_code,
300        })
301        .await?
302        .service_error_for_status()
303        .await?
304        .json()
305        .await
306    }
307}