libsignal_service/push_service/
registration.rs

1use derivative::Derivative;
2use libsignal_protocol::IdentityKey;
3use reqwest::Method;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use super::{AccountAttributes, PushService, ServiceError};
8use crate::{
9    configuration::Endpoint,
10    pre_keys::{KyberPreKeyEntity, SignedPreKeyEntity},
11    push_service::{response::ReqwestExt, HttpAuthOverride},
12    utils::serde_base64,
13};
14
15/// This type is used in registration lock handling.
16/// It's identical with HttpAuth, but used to avoid type confusion.
17#[derive(Derivative, Clone, Serialize, Deserialize)]
18#[derivative(Debug)]
19pub struct AuthCredentials {
20    pub username: String,
21    #[derivative(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 DeviceActivationRequest {
78    pub aci_signed_pre_key: SignedPreKeyEntity,
79    pub pni_signed_pre_key: SignedPreKeyEntity,
80    pub aci_pq_last_resort_pre_key: KyberPreKeyEntity,
81    pub pni_pq_last_resort_pre_key: KyberPreKeyEntity,
82}
83
84#[derive(Debug, Serialize)]
85pub struct CaptchaAttributes<'a> {
86    #[serde(rename = "type")]
87    pub challenge_type: &'a str,
88    pub token: &'a str,
89    pub captcha: &'a str,
90}
91
92#[derive(Debug, Clone, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct RegistrationSessionMetadataResponse {
95    pub id: String,
96    #[serde(default)]
97    pub next_sms: Option<i32>,
98    #[serde(default)]
99    pub next_call: Option<i32>,
100    #[serde(default)]
101    pub next_verification_attempt: Option<i32>,
102    pub allowed_to_request_code: bool,
103    #[serde(default)]
104    pub requested_information: Vec<String>,
105    pub verified: bool,
106}
107
108impl RegistrationSessionMetadataResponse {
109    pub fn push_challenge_required(&self) -> bool {
110        // .contains() requires &String ...
111        self.requested_information
112            .iter()
113            .any(|x| x.as_str() == "pushChallenge")
114    }
115
116    pub fn captcha_required(&self) -> bool {
117        // .contains() requires &String ...
118        self.requested_information
119            .iter()
120            .any(|x| x.as_str() == "captcha")
121    }
122}
123
124impl PushService {
125    pub async fn submit_registration_request(
126        &mut self,
127        registration_method: RegistrationMethod<'_>,
128        account_attributes: AccountAttributes,
129        skip_device_transfer: bool,
130        aci_identity_key: &IdentityKey,
131        pni_identity_key: &IdentityKey,
132        device_activation_request: DeviceActivationRequest,
133    ) -> Result<VerifyAccountResponse, ServiceError> {
134        #[derive(serde::Serialize, Debug)]
135        #[serde(rename_all = "camelCase")]
136        struct RegistrationSessionRequestBody<'a> {
137            // Unhandled response 422 with body:
138            // {"errors":["deviceActivationRequest.pniSignedPreKey must not be
139            // null","deviceActivationRequest.pniPqLastResortPreKey must not be
140            // null","everySignedKeyValid must be true","aciIdentityKey must not be
141            // null","pniIdentityKey must not be null","deviceActivationRequest.aciSignedPreKey
142            // must not be null","deviceActivationRequest.aciPqLastResortPreKey must not be null"]}
143            session_id: Option<&'a str>,
144            recovery_password: Option<&'a str>,
145            account_attributes: AccountAttributes,
146            skip_device_transfer: bool,
147            every_signed_key_valid: bool,
148            #[serde(default, with = "serde_base64")]
149            pni_identity_key: Vec<u8>,
150            #[serde(default, with = "serde_base64")]
151            aci_identity_key: Vec<u8>,
152            #[serde(flatten)]
153            device_activation_request: DeviceActivationRequest,
154        }
155
156        self.request(
157            Method::POST,
158            Endpoint::service("/v1/registration"),
159            HttpAuthOverride::NoOverride,
160        )?
161        .json(&RegistrationSessionRequestBody {
162            session_id: registration_method.session_id(),
163            recovery_password: registration_method.recovery_password(),
164            account_attributes,
165            skip_device_transfer,
166            aci_identity_key: aci_identity_key.serialize().into(),
167            pni_identity_key: pni_identity_key.serialize().into(),
168            device_activation_request,
169            every_signed_key_valid: true,
170        })
171        .send()
172        .await?
173        .service_error_for_status()
174        .await?
175        .json()
176        .await
177        .map_err(Into::into)
178    }
179
180    // Equivalent of Java's
181    // RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc)
182    pub async fn create_verification_session<'a>(
183        &mut self,
184        number: &'a str,
185        push_token: Option<&'a str>,
186        mcc: Option<&'a str>,
187        mnc: Option<&'a str>,
188    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
189        #[derive(serde::Serialize, Debug)]
190        #[serde(rename_all = "camelCase")]
191        struct VerificationSessionMetadataRequestBody<'a> {
192            number: &'a str,
193            push_token: Option<&'a str>,
194            mcc: Option<&'a str>,
195            mnc: Option<&'a str>,
196            push_token_type: Option<&'a str>,
197        }
198
199        self.request(
200            Method::POST,
201            Endpoint::service("/v1/verification/session"),
202            HttpAuthOverride::Unidentified,
203        )?
204        .json(&VerificationSessionMetadataRequestBody {
205            number,
206            push_token_type: push_token.as_ref().map(|_| "fcm"),
207            push_token,
208            mcc,
209            mnc,
210        })
211        .send()
212        .await?
213        .service_error_for_status()
214        .await?
215        .json()
216        .await
217        .map_err(Into::into)
218    }
219
220    pub async fn patch_verification_session<'a>(
221        &mut self,
222        session_id: &'a str,
223        push_token: Option<&'a str>,
224        mcc: Option<&'a str>,
225        mnc: Option<&'a str>,
226        captcha: Option<&'a str>,
227        push_challenge: Option<&'a str>,
228    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
229        #[derive(serde::Serialize, Debug)]
230        #[serde(rename_all = "camelCase")]
231        struct UpdateVerificationSessionRequestBody<'a> {
232            captcha: Option<&'a str>,
233            push_token: Option<&'a str>,
234            push_challenge: Option<&'a str>,
235            mcc: Option<&'a str>,
236            mnc: Option<&'a str>,
237            push_token_type: Option<&'a str>,
238        }
239
240        self.request(
241            Method::PATCH,
242            Endpoint::service(format!(
243                "/v1/verification/session/{}",
244                session_id
245            )),
246            HttpAuthOverride::Unidentified,
247        )?
248        .json(&UpdateVerificationSessionRequestBody {
249            captcha,
250            push_token_type: push_token.as_ref().map(|_| "fcm"),
251            push_token,
252            mcc,
253            mnc,
254            push_challenge,
255        })
256        .send()
257        .await?
258        .service_error_for_status()
259        .await?
260        .json()
261        .await
262        .map_err(Into::into)
263    }
264
265    // Equivalent of Java's
266    // RegistrationSessionMetadataResponse requestVerificationCode(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport)
267    /// Request a verification code.
268    ///
269    /// Signal requires a client type, and they use these three strings internally:
270    ///   - "android-2021-03"
271    ///   - "android"
272    ///   - "ios"
273    ///
274    /// "android-2021-03" allegedly implies FCM support, whereas the other strings don't. In
275    /// principle, they will consider any string as "unknown", so other strings may work too.
276    pub async fn request_verification_code(
277        &mut self,
278        session_id: &str,
279        client: &str,
280        // XXX: We currently don't support this, because we need to set some headers in the
281        //      post_json() call
282        // locale: Option<String>,
283        transport: VerificationTransport,
284    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
285        #[derive(Debug, Serialize)]
286        struct VerificationCodeRequest<'a> {
287            transport: VerificationTransport,
288            client: &'a str,
289        }
290
291        self.request(
292            Method::POST,
293            Endpoint::service(format!(
294                "/v1/verification/session/{}/code",
295                session_id
296            )),
297            HttpAuthOverride::Unidentified,
298        )?
299        .json(&VerificationCodeRequest { transport, client })
300        .send()
301        .await?
302        .service_error_for_status()
303        .await?
304        .json()
305        .await
306        .map_err(Into::into)
307    }
308
309    pub async fn submit_verification_code(
310        &mut self,
311        session_id: &str,
312        verification_code: &str,
313    ) -> Result<RegistrationSessionMetadataResponse, ServiceError> {
314        #[derive(Debug, Serialize)]
315        struct VerificationCode<'a> {
316            code: &'a str,
317        }
318
319        self.request(
320            Method::PUT,
321            Endpoint::service(format!(
322                "/v1/verification/session/{}/code",
323                session_id
324            )),
325            HttpAuthOverride::Unidentified,
326        )?
327        .json(&VerificationCode {
328            code: verification_code,
329        })
330        .send()
331        .await?
332        .service_error_for_status()
333        .await?
334        .json()
335        .await
336        .map_err(Into::into)
337    }
338}