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    utils::{serde_base64, TryIntoE164},
13    websocket::{self, account::AccountAttributes, SignalWebSocket},
14};
15
16/// This type is used in registration lock handling.
17/// It's identical with HttpAuth, but used to avoid type confusion.
18#[derive(derive_more::Debug, Clone, Serialize, Deserialize)]
19pub struct AuthCredentials {
20    pub username: String,
21    #[debug(ignore)]
22    pub password: String,
23}
24
25#[derive(Debug, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct RegistrationLockFailure {
28    pub length: Option<u32>,
29    pub time_remaining: Option<u64>,
30    #[serde(rename = "backup_credentials")]
31    pub svr1_credentials: Option<AuthCredentials>,
32    pub svr2_credentials: Option<AuthCredentials>,
33}
34
35#[derive(Debug, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct VerifyAccountResponse {
38    #[serde(rename = "uuid")]
39    pub aci: Uuid,
40    pub pni: Uuid,
41    pub storage_capable: bool,
42    #[serde(default)]
43    pub number: Option<String>,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
47#[serde(rename_all = "snake_case")]
48pub enum VerificationTransport {
49    Sms,
50    Voice,
51}
52
53#[derive(Clone, Debug)]
54pub enum RegistrationMethod<'a> {
55    SessionId(&'a str),
56    RecoveryPassword(&'a str),
57}
58
59impl<'a> RegistrationMethod<'a> {
60    pub fn session_id(&'a self) -> Option<&'a str> {
61        match self {
62            Self::SessionId(x) => Some(x),
63            _ => None,
64        }
65    }
66
67    pub fn recovery_password(&'a self) -> Option<&'a str> {
68        match self {
69            Self::RecoveryPassword(x) => Some(x),
70            _ => None,
71        }
72    }
73}
74
75#[derive(Debug, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct RegistrationKeyPackage {
78    pub aci_identity_key: Vec<u8>,
79    pub pni_identity_key: Vec<u8>,
80    pub aci_signed_pre_key: SignedPreKeyEntity,
81    pub pni_signed_pre_key: SignedPreKeyEntity,
82    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
83    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
84}
85
86#[derive(Debug, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct DeviceActivationRequest {
89    pub aci_signed_pre_key: SignedPreKeyEntity,
90    pub pni_signed_pre_key: SignedPreKeyEntity,
91    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
92    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
93}
94
95#[derive(Debug, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct GcmRegistrationId<'a> {
98    pub gcm_registration_id: &'a str,
99    pub web_socket_channel: bool,
100}
101
102#[derive(Debug, Serialize)]
103pub struct CaptchaAttributes<'a> {
104    #[serde(rename = "type")]
105    pub challenge_type: &'a str,
106    pub token: &'a str,
107    pub captcha: &'a str,
108}
109
110#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct RegistrationSessionMetadataResponse {
113    pub id: String,
114    #[serde(default)]
115    pub next_sms: Option<i32>,
116    #[serde(default)]
117    pub next_call: Option<i32>,
118    #[serde(default)]
119    pub next_verification_attempt: Option<i32>,
120    pub allowed_to_request_code: bool,
121    #[serde(default)]
122    pub requested_information: Vec<String>,
123    pub verified: bool,
124}
125
126impl RegistrationSessionMetadataResponse {
127    pub fn push_challenge_required(&self) -> bool {
128        // .contains() requires &String ...
129        self.requested_information
130            .iter()
131            .any(|x| x.as_str() == "pushChallenge")
132    }
133
134    pub fn captcha_required(&self) -> bool {
135        // .contains() requires &String ...
136        self.requested_information
137            .iter()
138            .any(|x| x.as_str() == "captcha")
139    }
140}
141
142impl SignalWebSocket<websocket::Unidentified> {
143    // Equivalent of Java's
144    // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc)
145    pub async fn create_verification_session<'a>(
146        &mut self,
147        number: &'a str,
148        push_token: Option<&'a str>,
149        mcc: Option<&'a str>,
150        mnc: Option<&'a str>,
151    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
152        #[derive(serde::Serialize, Debug)]
153        #[serde(rename_all = "camelCase")]
154        struct VerificationSessionMetadataRequestBody<'a> {
155            number: &'a str,
156            push_token: Option<&'a str>,
157            mcc: Option<&'a str>,
158            mnc: Option<&'a str>,
159            push_token_type: Option<&'a str>,
160        }
161
162        self.http_request(Method::POST, "/v1/verification/session")?
163            .send_json(&VerificationSessionMetadataRequestBody {
164                number,
165                push_token_type: push_token.as_ref().map(|_| "fcm"),
166                push_token,
167                mcc,
168                mnc,
169            })
170            .await?
171            .service_error_for_status()
172            .await?
173            .json()
174            .await
175    }
176
177    pub async fn patch_verification_session<'a>(
178        &mut self,
179        session_id: &'a str,
180        push_token: Option<&'a str>,
181        mcc: Option<&'a str>,
182        mnc: Option<&'a str>,
183        captcha: Option<&'a str>,
184        push_challenge: Option<&'a str>,
185    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
186        #[derive(serde::Serialize, Debug)]
187        #[serde(rename_all = "camelCase")]
188        struct UpdateVerificationSessionRequestBody<'a> {
189            captcha: Option<&'a str>,
190            push_token: Option<&'a str>,
191            push_challenge: Option<&'a str>,
192            mcc: Option<&'a str>,
193            mnc: Option<&'a str>,
194            push_token_type: Option<&'a str>,
195        }
196
197        self.http_request(
198            Method::PATCH,
199            format!("/v1/verification/session/{}", session_id),
200        )?
201        .send_json(&UpdateVerificationSessionRequestBody {
202            captcha,
203            push_token_type: push_token.as_ref().map(|_| "fcm"),
204            push_token,
205            mcc,
206            mnc,
207            push_challenge,
208        })
209        .await?
210        .service_error_for_status()
211        .await?
212        .json()
213        .await
214    }
215
216    // Equivalent of Java's
217    // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport)
218    /// Request a verification code.
219    ///
220    /// Signal requires a client type, and they use these three strings internally:
221    ///   - "android-2021-03"
222    ///   - "android"
223    ///   - "ios"
224    ///
225    /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In
226    /// principle, they will consider any string as "unknown", so other strings may work too.
227    pub async fn request_verification_code(
228        &mut self,
229        session_id: &str,
230        client: &str,
231        // XXX: We currently don't support this, because we need to set some headers in the
232        //      post_json() call
233        // locale: Option<String>,
234        transport: VerificationTransport,
235    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
236        #[derive(Debug, Serialize)]
237        struct VerificationCodeRequest<'a> {
238            transport: VerificationTransport,
239            client: &'a str,
240        }
241
242        self.http_request(
243            Method::POST,
244            format!("/v1/verification/session/{}/code", session_id),
245        )?
246        .send_json(&VerificationCodeRequest { transport, client })
247        .await?
248        .service_error_for_status()
249        .await?
250        .json()
251        .await
252    }
253
254    #[allow(clippy::too_many_arguments)]
255    pub async fn submit_registration_request(
256        &mut self,
257        registration_method: RegistrationMethod<'_>,
258        gcm_token: Option<GcmRegistrationId<'_>>,
259        phonenumber: impl TryIntoE164,
260        password: &str,
261        account_attributes: AccountAttributes,
262        skip_device_transfer: bool,
263        keys: RegistrationKeyPackage,
264    ) -> Result<VerifyAccountResponse, ServiceError> {
265        #[derive(serde::Serialize, Debug)]
266        #[serde(rename_all = "camelCase")]
267        /// https://github.com/signalapp/Signal-Android/blob/main/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt
268        struct RegistrationSessionRequestBody<'a> {
269            session_id: Option<&'a str>,
270            recovery_password: Option<&'a str>,
271            account_attributes: AccountAttributes,
272            skip_device_transfer: bool,
273            #[serde(default, with = "serde_base64")]
274            pni_identity_key: Vec<u8>,
275            #[serde(default, with = "serde_base64")]
276            aci_identity_key: Vec<u8>,
277            aci_signed_pre_key: SignedPreKeyEntity,
278            pni_signed_pre_key: SignedPreKeyEntity,
279            aci_pq_last_resort_pre_key: KyberPreKeyEntity,
280            pni_pq_last_resort_pre_key: KyberPreKeyEntity,
281            gcm_token: Option<GcmRegistrationId<'a>>,
282            require_atomic: bool,
283        }
284
285        let phonenumber = phonenumber
286            .try_into_e164()
287            .map_err(|_| ServiceError::InvalidPhoneNumber)?;
288
289        self.http_request(Method::POST, "/v1/registration")?
290            .registration_auth_header(phonenumber, password)
291            .send_json(&RegistrationSessionRequestBody {
292                session_id: registration_method.session_id(),
293                recovery_password: registration_method.recovery_password(),
294                account_attributes,
295                skip_device_transfer,
296                aci_identity_key: keys.aci_identity_key,
297                pni_identity_key: keys.pni_identity_key,
298                aci_signed_pre_key: keys.aci_signed_pre_key,
299                pni_signed_pre_key: keys.pni_signed_pre_key,
300                aci_pq_last_resort_pre_key: keys.aci_pq_last_resort_pre_key,
301                pni_pq_last_resort_pre_key: keys.pni_pq_last_resort_pre_key,
302                gcm_token,
303                require_atomic: true, // XXX default = true but what does this signify?
304            })
305            .await?
306            .service_error_for_status()
307            .await?
308            .json()
309            .await
310    }
311
312    pub async fn submit_verification_code(
313        &mut self,
314        session_id: &str,
315        verification_code: &str,
316    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
317        #[derive(Debug, Serialize)]
318        struct VerificationCode<'a> {
319            code: &'a str,
320        }
321
322        self.http_request(
323            Method::PUT,
324            format!("/v1/verification/session/{}/code", session_id),
325        )?
326        .send_json(&VerificationCode {
327            code: verification_code,
328        })
329        .await?
330        .service_error_for_status()
331        .await?
332        .json()
333        .await
334    }
335
336    #[allow(clippy::too_many_arguments)]
337    pub async fn register_account<
338        R: Rng + CryptoRng,
339        Aci: PreKeysStore + IdentityKeyStore,
340        Pni: PreKeysStore + IdentityKeyStore,
341    >(
342        &mut self,
343        csprng: &mut R,
344        registration_method: RegistrationMethod<'_>,
345        gcm_token: Option<GcmRegistrationId<'_>>,
346        account_attributes: AccountAttributes,
347        aci_protocol_store: &mut Aci,
348        pni_protocol_store: &mut Pni,
349        skip_device_transfer: bool,
350        phonenumber: impl TryIntoE164,
351        password: &str,
352    ) -> Result<VerifyAccountResponse, ProvisioningError> {
353        let aci_identity_key_pair = aci_protocol_store
354            .get_identity_key_pair()
355            .instrument(tracing::trace_span!("get ACI identity key pair"))
356            .await?;
357        let pni_identity_key_pair = pni_protocol_store
358            .get_identity_key_pair()
359            .instrument(tracing::trace_span!("get PNI identity key pair"))
360            .await?;
361
362        let (
363            _aci_pre_keys,
364            aci_signed_pre_key,
365            _aci_kyber_pre_keys,
366            aci_last_resort_kyber_prekey,
367        ) = crate::pre_keys::replenish_pre_keys(
368            aci_protocol_store,
369            csprng,
370            &aci_identity_key_pair,
371            true,
372            0,
373            0,
374        )
375        .await?;
376
377        let (
378            _pni_pre_keys,
379            pni_signed_pre_key,
380            _pni_kyber_pre_keys,
381            pni_last_resort_kyber_prekey,
382        ) = crate::pre_keys::replenish_pre_keys(
383            pni_protocol_store,
384            csprng,
385            &pni_identity_key_pair,
386            true,
387            0,
388            0,
389        )
390        .await?;
391
392        let aci_identity_key = aci_identity_key_pair.identity_key();
393        let pni_identity_key = pni_identity_key_pair.identity_key();
394        let keys = RegistrationKeyPackage {
395            aci_identity_key: aci_identity_key.serialize().into(),
396            pni_identity_key: pni_identity_key.serialize().into(),
397            aci_signed_pre_key: SignedPreKeyEntity::try_from(
398                &aci_signed_pre_key,
399            )
400            .unwrap(),
401            pni_signed_pre_key: pni_signed_pre_key.try_into()?,
402            aci_pq_last_resort_pre_key: aci_last_resort_kyber_prekey
403                .expect("requested last resort prekey")
404                .try_into()?,
405            pni_pq_last_resort_pre_key: pni_last_resort_kyber_prekey
406                .expect("requested last resort prekey")
407                .try_into()?,
408        };
409
410        let result = self
411            .submit_registration_request(
412                registration_method,
413                gcm_token,
414                phonenumber,
415                password,
416                account_attributes,
417                skip_device_transfer,
418                keys,
419            )
420            .await?;
421
422        Ok(result)
423    }
424}