Skip to main content

libsignal_service/websocket/
registration.rs

1use libsignal_protocol::IdentityKeyStore;
2use rand::{CryptoRng, Rng};
3use reqwest::Method;
4use serde::{Deserialize, Serialize};
5use tracing::Instrument;
6use uuid::Uuid;
7
8use super::ServiceError;
9use crate::{
10    pre_keys::{KyberPreKeyEntity, PreKeysStore, SignedPreKeyEntity},
11    provisioning::ProvisioningError,
12    push_service::response::{
13        error_mapper, json_or_unhandled, parse_retry_after,
14        SignalServiceResponse,
15    },
16    utils::{serde_base64, TryIntoE164},
17    websocket::{self, account::AccountAttributes, SignalWebSocket},
18};
19
20/// This type is used in registration lock handling.
21/// It's identical with HttpAuth, but used to avoid type confusion.
22#[derive(derive_more::Debug, Clone, Serialize, Deserialize)]
23pub struct AuthCredentials {
24    pub username: String,
25    #[debug(ignore)]
26    pub password: String,
27}
28
29#[derive(Debug, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct RegistrationLockFailure {
32    pub length: Option<u32>,
33    pub time_remaining: Option<u64>,
34    #[serde(rename = "backup_credentials")]
35    pub svr1_credentials: Option<AuthCredentials>,
36    pub svr2_credentials: Option<AuthCredentials>,
37}
38
39#[derive(Debug, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct VerifyAccountResponse {
42    #[serde(rename = "uuid")]
43    pub aci: Uuid,
44    pub pni: Uuid,
45    pub storage_capable: bool,
46    #[serde(default)]
47    pub number: Option<String>,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
51#[serde(rename_all = "snake_case")]
52pub enum VerificationTransport {
53    Sms,
54    Voice,
55}
56
57#[derive(Clone, Debug)]
58pub enum RegistrationMethod<'a> {
59    SessionId(&'a str),
60    RecoveryPassword(&'a str),
61}
62
63impl<'a> RegistrationMethod<'a> {
64    pub fn session_id(&'a self) -> Option<&'a str> {
65        match self {
66            Self::SessionId(x) => Some(x),
67            _ => None,
68        }
69    }
70
71    pub fn recovery_password(&'a self) -> Option<&'a str> {
72        match self {
73            Self::RecoveryPassword(x) => Some(x),
74            _ => None,
75        }
76    }
77}
78
79#[derive(Debug, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct RegistrationKeyPackage {
82    pub aci_identity_key: Vec<u8>,
83    pub pni_identity_key: Vec<u8>,
84    pub aci_signed_pre_key: SignedPreKeyEntity,
85    pub pni_signed_pre_key: SignedPreKeyEntity,
86    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
87    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
88}
89
90#[derive(Debug, Serialize)]
91#[serde(rename_all = "camelCase")]
92pub struct DeviceActivationRequest {
93    pub aci_signed_pre_key: SignedPreKeyEntity,
94    pub pni_signed_pre_key: SignedPreKeyEntity,
95    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
96    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
97}
98
99#[derive(Debug, Serialize)]
100#[serde(rename_all = "camelCase")]
101pub struct GcmRegistrationId<'a> {
102    pub gcm_registration_id: &'a str,
103    pub web_socket_channel: bool,
104}
105
106#[derive(Debug, Serialize)]
107pub struct CaptchaAttributes<'a> {
108    #[serde(rename = "type")]
109    pub challenge_type: &'a str,
110    pub token: &'a str,
111    pub captcha: &'a str,
112}
113
114#[derive(Debug, Clone, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct RegistrationSessionMetadataResponse {
117    pub id: String,
118    #[serde(default)]
119    pub next_sms: Option<i32>,
120    #[serde(default)]
121    pub next_call: Option<i32>,
122    #[serde(default)]
123    pub next_verification_attempt: Option<i32>,
124    pub allowed_to_request_code: bool,
125    #[serde(default)]
126    pub requested_information: Vec<String>,
127    pub verified: bool,
128}
129
130impl RegistrationSessionMetadataResponse {
131    pub fn push_challenge_required(&self) -> bool {
132        // .contains() requires &String ...
133        self.requested_information
134            .iter()
135            .any(|x| x.as_str() == "pushChallenge")
136    }
137
138    pub fn captcha_required(&self) -> bool {
139        // .contains() requires &String ...
140        self.requested_information
141            .iter()
142            .any(|x| x.as_str() == "captcha")
143    }
144}
145
146// 429: specialised through the response's `retry-after` header plus the
147// session metadata body; see VerificationSessionRateLimited.
148async fn session_rate_limited<R>(response: R) -> ServiceError
149where
150    R: SignalServiceResponse,
151    ServiceError: From<<R as SignalServiceResponse>::Error>,
152{
153    let retry_after =
154        response.header("retry-after").and_then(parse_retry_after);
155    match json_or_unhandled::<R, RegistrationSessionMetadataResponse>(response)
156        .await
157    {
158        Ok(session) => ServiceError::VerificationSessionRateLimited {
159            session,
160            retry_after,
161        },
162        Err(error) => error,
163    }
164}
165
166// Signal-Server: controllers/VerificationController.java:196
167// (POST /v1/verification/session)
168error_mapper! {
169    create_verification_session_errors:
170        // 429: session rate limited, VerificationController.java:795
171        TOO_MANY_REQUESTS => fn session_rate_limited,
172}
173
174// Signal-Server: controllers/VerificationController.java:275
175// (PATCH /v1/verification/session/{sessionId})
176error_mapper! {
177    patch_verification_session_errors:
178        // 403: token not accepted, VerificationController.java:315
179        FORBIDDEN => TokenNotAccepted(RegistrationSessionMetadataResponse),
180        // 404: no such session, VerificationController.java:820
181        NOT_FOUND => NoSuchSession,
182        // 422: invalid session id, VerificationController.java:815
183        UNPROCESSABLE_ENTITY => InvalidVerificationSessionId,
184        // 429: session rate limited, VerificationController.java:309
185        TOO_MANY_REQUESTS => fn session_rate_limited,
186}
187
188// Signal-Server: controllers/VerificationController.java:576
189// (POST /v1/verification/session/{sessionId}/code)
190error_mapper! {
191    request_verification_code_errors:
192        // 404: no such session, VerificationController.java:654
193        NOT_FOUND => NoSuchSession,
194        // 409: session conflict, VerificationController.java:598
195        CONFLICT => RegistrationSessionConflict(RegistrationSessionMetadataResponse),
196        // 418: transport not allowed, VerificationController.java:649
197        IM_A_TEAPOT => InvalidTransportMode(RegistrationSessionMetadataResponse),
198        // 422: invalid session id, VerificationController.java:815
199        UNPROCESSABLE_ENTITY => InvalidVerificationSessionId,
200        // 429: session rate limited, VerificationController.java:641
201        TOO_MANY_REQUESTS => fn session_rate_limited,
202        // 440: remote service rejected code delivery, VerificationController.java:568
203        // (RegistrationServiceSenderExceptionMapper.java:15)
204        440 => VerificationDeliveryFailed(crate::push_service::VerificationDeliveryFailure),
205}
206
207// Signal-Server: controllers/VerificationController.java:714
208// (PUT /v1/verification/session/{sessionId}/code)
209error_mapper! {
210    submit_verification_code_errors:
211        // 404: no such session, VerificationController.java:743
212        NOT_FOUND => NoSuchSession,
213        // 409: session conflict, VerificationController.java:726
214        CONFLICT => RegistrationSessionConflict(RegistrationSessionMetadataResponse),
215        // 422: invalid session id, VerificationController.java:815
216        UNPROCESSABLE_ENTITY => InvalidVerificationSessionId,
217        // 429: session rate limited, VerificationController.java:735
218        TOO_MANY_REQUESTS => fn session_rate_limited,
219}
220
221// Signal-Server: controllers/RegistrationController.java:114
222// (PUT /v1/registration)
223error_mapper! {
224    post_registration_errors:
225        // 409: device transfer available, RegistrationController.java:161
226        CONFLICT => DeviceTransferAvailable,
227}
228
229impl SignalWebSocket<websocket::Unidentified> {
230    // Equivalent of Java's
231    // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc)
232    pub async fn create_verification_session<'a>(
233        &mut self,
234        number: &'a str,
235        push_token: Option<&'a str>,
236        mcc: Option<&'a str>,
237        mnc: Option<&'a str>,
238    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
239        #[derive(serde::Serialize, Debug)]
240        #[serde(rename_all = "camelCase")]
241        struct VerificationSessionMetadataRequestBody<'a> {
242            number: &'a str,
243            push_token: Option<&'a str>,
244            mcc: Option<&'a str>,
245            mnc: Option<&'a str>,
246            push_token_type: Option<&'a str>,
247        }
248
249        self.http_request(Method::POST, "/v1/verification/session")?
250            .send_json(&VerificationSessionMetadataRequestBody {
251                number,
252                push_token_type: push_token.as_ref().map(|_| "fcm"),
253                push_token,
254                mcc,
255                mnc,
256            })
257            .await?
258            .service_error_for_status_with(create_verification_session_errors)
259            .await?
260            .json()
261            .await
262    }
263
264    pub async fn patch_verification_session<'a>(
265        &mut self,
266        session_id: &'a str,
267        push_token: Option<&'a str>,
268        mcc: Option<&'a str>,
269        mnc: Option<&'a str>,
270        captcha: Option<&'a str>,
271        push_challenge: Option<&'a str>,
272    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
273        #[derive(serde::Serialize, Debug)]
274        #[serde(rename_all = "camelCase")]
275        struct UpdateVerificationSessionRequestBody<'a> {
276            captcha: Option<&'a str>,
277            push_token: Option<&'a str>,
278            push_challenge: Option<&'a str>,
279            mcc: Option<&'a str>,
280            mnc: Option<&'a str>,
281            push_token_type: Option<&'a str>,
282        }
283
284        self.http_request(
285            Method::PATCH,
286            format!("/v1/verification/session/{}", session_id),
287        )?
288        .send_json(&UpdateVerificationSessionRequestBody {
289            captcha,
290            push_token_type: push_token.as_ref().map(|_| "fcm"),
291            push_token,
292            mcc,
293            mnc,
294            push_challenge,
295        })
296        .await?
297        .service_error_for_status_with(patch_verification_session_errors)
298        .await?
299        .json()
300        .await
301    }
302
303    // Equivalent of Java's
304    // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport)
305    /// Request a verification code.
306    ///
307    /// Signal requires a client type, and they use these three strings internally:
308    ///   - "android-2021-03"
309    ///   - "android"
310    ///   - "ios"
311    ///
312    /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In
313    /// principle, they will consider any string as "unknown", so other strings may work too.
314    pub async fn request_verification_code(
315        &mut self,
316        session_id: &str,
317        client: &str,
318        // XXX: We currently don't support this, because we need to set some headers in the
319        //      post_json() call
320        // locale: Option<String>,
321        transport: VerificationTransport,
322    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
323        #[derive(Debug, Serialize)]
324        struct VerificationCodeRequest<'a> {
325            transport: VerificationTransport,
326            client: &'a str,
327        }
328
329        self.http_request(
330            Method::POST,
331            format!("/v1/verification/session/{}/code", session_id),
332        )?
333        .send_json(&VerificationCodeRequest { transport, client })
334        .await?
335        .service_error_for_status_with(request_verification_code_errors)
336        .await?
337        .json()
338        .await
339    }
340
341    #[allow(clippy::too_many_arguments)]
342    pub async fn submit_registration_request(
343        &mut self,
344        registration_method: RegistrationMethod<'_>,
345        gcm_token: Option<GcmRegistrationId<'_>>,
346        phonenumber: impl TryIntoE164,
347        password: &str,
348        account_attributes: AccountAttributes,
349        skip_device_transfer: bool,
350        keys: RegistrationKeyPackage,
351    ) -> Result<VerifyAccountResponse, ServiceError> {
352        #[derive(serde::Serialize, Debug)]
353        #[serde(rename_all = "camelCase")]
354        /// https://github.com/signalapp/Signal-Android/blob/main/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt
355        struct RegistrationSessionRequestBody<'a> {
356            session_id: Option<&'a str>,
357            recovery_password: Option<&'a str>,
358            account_attributes: AccountAttributes,
359            skip_device_transfer: bool,
360            #[serde(default, with = "serde_base64")]
361            pni_identity_key: Vec<u8>,
362            #[serde(default, with = "serde_base64")]
363            aci_identity_key: Vec<u8>,
364            aci_signed_pre_key: SignedPreKeyEntity,
365            pni_signed_pre_key: SignedPreKeyEntity,
366            aci_pq_last_resort_pre_key: KyberPreKeyEntity,
367            pni_pq_last_resort_pre_key: KyberPreKeyEntity,
368            gcm_token: Option<GcmRegistrationId<'a>>,
369            require_atomic: bool,
370        }
371
372        let phonenumber = phonenumber
373            .try_into_e164()
374            .map_err(|_| ServiceError::InvalidPhoneNumber)?;
375
376        self.http_request(Method::POST, "/v1/registration")?
377            .registration_auth_header(phonenumber, password)
378            .send_json(&RegistrationSessionRequestBody {
379                session_id: registration_method.session_id(),
380                recovery_password: registration_method.recovery_password(),
381                account_attributes,
382                skip_device_transfer,
383                aci_identity_key: keys.aci_identity_key,
384                pni_identity_key: keys.pni_identity_key,
385                aci_signed_pre_key: keys.aci_signed_pre_key,
386                pni_signed_pre_key: keys.pni_signed_pre_key,
387                aci_pq_last_resort_pre_key: keys.aci_pq_last_resort_pre_key,
388                pni_pq_last_resort_pre_key: keys.pni_pq_last_resort_pre_key,
389                gcm_token,
390                require_atomic: true, // XXX default = true but what does this signify?
391            })
392            .await?
393            .service_error_for_status_with(post_registration_errors)
394            .await?
395            .json()
396            .await
397    }
398
399    pub async fn submit_verification_code(
400        &mut self,
401        session_id: &str,
402        verification_code: &str,
403    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
404        #[derive(Debug, Serialize)]
405        struct VerificationCode<'a> {
406            code: &'a str,
407        }
408
409        self.http_request(
410            Method::PUT,
411            format!("/v1/verification/session/{}/code", session_id),
412        )?
413        .send_json(&VerificationCode {
414            code: verification_code,
415        })
416        .await?
417        .service_error_for_status_with(submit_verification_code_errors)
418        .await?
419        .json()
420        .await
421    }
422
423    #[allow(clippy::too_many_arguments)]
424    pub async fn register_account<
425        R: Rng + CryptoRng,
426        Aci: PreKeysStore + IdentityKeyStore,
427        Pni: PreKeysStore + IdentityKeyStore,
428    >(
429        &mut self,
430        csprng: &mut R,
431        registration_method: RegistrationMethod<'_>,
432        gcm_token: Option<GcmRegistrationId<'_>>,
433        account_attributes: AccountAttributes,
434        aci_protocol_store: &mut Aci,
435        pni_protocol_store: &mut Pni,
436        skip_device_transfer: bool,
437        phonenumber: impl TryIntoE164,
438        password: &str,
439    ) -> Result<VerifyAccountResponse, ProvisioningError> {
440        let aci_identity_key_pair = aci_protocol_store
441            .get_identity_key_pair()
442            .instrument(tracing::trace_span!("get ACI identity key pair"))
443            .await?;
444        let pni_identity_key_pair = pni_protocol_store
445            .get_identity_key_pair()
446            .instrument(tracing::trace_span!("get PNI identity key pair"))
447            .await?;
448
449        let (
450            _aci_pre_keys,
451            aci_signed_pre_key,
452            _aci_kyber_pre_keys,
453            aci_last_resort_kyber_prekey,
454        ) = crate::pre_keys::replenish_pre_keys(
455            aci_protocol_store,
456            csprng,
457            &aci_identity_key_pair,
458            true,
459            0,
460            0,
461        )
462        .await?;
463
464        let (
465            _pni_pre_keys,
466            pni_signed_pre_key,
467            _pni_kyber_pre_keys,
468            pni_last_resort_kyber_prekey,
469        ) = crate::pre_keys::replenish_pre_keys(
470            pni_protocol_store,
471            csprng,
472            &pni_identity_key_pair,
473            true,
474            0,
475            0,
476        )
477        .await?;
478
479        let aci_identity_key = aci_identity_key_pair.identity_key();
480        let pni_identity_key = pni_identity_key_pair.identity_key();
481        let keys = RegistrationKeyPackage {
482            aci_identity_key: aci_identity_key.serialize().into(),
483            pni_identity_key: pni_identity_key.serialize().into(),
484            aci_signed_pre_key: SignedPreKeyEntity::try_from(
485                &aci_signed_pre_key,
486            )
487            .unwrap(),
488            pni_signed_pre_key: pni_signed_pre_key.try_into()?,
489            aci_pq_last_resort_pre_key: aci_last_resort_kyber_prekey
490                .expect("requested last resort prekey")
491                .try_into()?,
492            pni_pq_last_resort_pre_key: pni_last_resort_kyber_prekey
493                .expect("requested last resort prekey")
494                .try_into()?,
495        };
496
497        let result = self
498            .submit_registration_request(
499                registration_method,
500                gcm_token,
501                phonenumber,
502                password,
503                account_attributes,
504                skip_device_transfer,
505                keys,
506            )
507            .await?;
508
509        Ok(result)
510    }
511}