pass_fxa_lib/
lib.rs

1use aes::Aes256;
2use block_modes::{block_padding::Pkcs7, BlockMode, Cbc};
3use futures::{stream::FuturesUnordered, StreamExt};
4use hkdf::Hkdf;
5use hmac::{Hmac, Mac, NewMac};
6use rand::{rngs::OsRng, RngCore};
7use reqwest::{header, Request, StatusCode};
8use rsa::{hash::Hash, padding::PaddingScheme, PublicKeyParts, RSAPrivateKey};
9use secstr::SecUtf8;
10use serde::{de, Deserialize, Serialize, Serializer};
11use sha2::{Digest, Sha256};
12use std::{
13    collections::HashMap,
14    io::{self, Write},
15};
16use tokio::time::{sleep, Duration};
17use url::Url;
18
19const DURATION: u64 = 60;
20const BATCH_SIZE: usize = 100;
21
22#[derive(Deserialize)]
23struct CryptoKeyRecord {
24    default: Vec<String>,
25}
26
27pub trait BsoObject {
28    fn id(&self) -> &str;
29}
30
31fn origin_serialize<S: Serializer>(hostname: &Url, s: S) -> Result<S::Ok, S::Error> {
32    s.serialize_str(&hostname.origin().ascii_serialization())
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone)]
36#[serde(rename_all = "camelCase")]
37pub struct Login {
38    id: String,
39    #[serde(serialize_with = "origin_serialize")]
40    pub hostname: Url,
41    #[serde(rename = "formSubmitURL")]
42    form_submit_url: String,
43    http_realm: Option<String>,
44    pub username: String,
45    pub password: SecUtf8,
46    // HTML tag id of the username field
47    username_field: String,
48    // HTML tag id of the password field
49    password_field: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    time_last_used: Option<u64>,
52    // TODO: update this
53    #[serde(skip_serializing_if = "Option::is_none")]
54    time_created: Option<u64>,
55    // TODO: update this
56    #[serde(skip_serializing_if = "Option::is_none")]
57    time_password_changed: Option<u64>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    time_used: Option<u64>,
60}
61
62impl BsoObject for Login {
63    fn id(&self) -> &str {
64        &self.id
65    }
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69struct Deleted {
70    id: String,
71    deleted: bool,
72}
73
74impl BsoObject for Deleted {
75    fn id(&self) -> &str {
76        &self.id
77    }
78}
79
80#[derive(Serialize, Deserialize, Debug)]
81#[serde(untagged)]
82enum PasswordBSORecord {
83    Password(Login),
84    Deleted(Deleted),
85}
86
87fn generate_bso_id() -> String {
88    let bytes: [u8; 9] = rand::random();
89    base64::encode_config(bytes, base64::URL_SAFE_NO_PAD)
90}
91
92impl Login {
93    pub fn new(username: &str, password: &str, hostname: Url) -> Self {
94        Self {
95            id: generate_bso_id(),
96            hostname,
97            form_submit_url: String::new(),
98            http_realm: None,
99            username: username.to_string(),
100            password: password.into(),
101            username_field: String::new(),
102            password_field: String::new(),
103            time_created: None,
104            time_last_used: None,
105            time_password_changed: None,
106            time_used: None,
107        }
108    }
109
110    pub fn with_password(&self, new_password: &str) -> Self {
111        Self {
112            password: new_password.into(),
113            ..self.clone()
114        }
115    }
116}
117
118#[derive(Serialize)]
119#[serde(rename_all = "camelCase")]
120struct AccountLoginRequest<'a, 'b, 'c> {
121    email: &'a str,
122    #[serde(rename = "authPW")]
123    auth_pw: &'a str,
124    reason: &'a str,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    unblock_code: Option<&'b str>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    verification_method: Option<&'c str>,
129}
130
131#[derive(Deserialize)]
132struct SyncServerToken {
133    id: String,
134    key: String,
135    uid: u64,
136    api_endpoint: String,
137    duration: u32,
138    hashalg: String,
139    hashed_fxa_uid: String,
140    node_type: String,
141}
142
143#[derive(Deserialize, Serialize)]
144struct Payload {
145    ciphertext: String,
146    #[serde(rename = "IV")]
147    iv: String,
148    hmac: String,
149}
150
151type Aes256Cbc = Cbc<Aes256, Pkcs7>;
152#[derive(Deserialize, Serialize)]
153struct BSO {
154    id: String,
155    //modified: f64,
156    #[serde(with = "serde_with::json::nested")]
157    payload: Payload,
158}
159
160impl BSO {
161    fn from_object(object: &(impl BsoObject + Serialize), key: &[u8], hmac_key: &[u8]) -> Self {
162        let iv = generate_iv();
163        let cipher = Aes256Cbc::new_from_slices(key, &iv).unwrap();
164        let mut payload = serde_json::to_vec(&object).unwrap();
165        let plaintext_len = payload.len();
166        payload.extend_from_slice(&[0u8; 16][0..16 - plaintext_len % 16]);
167        cipher.encrypt(&mut payload, plaintext_len).unwrap();
168        let ciphertext_base64 = base64::encode(payload);
169        let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key).unwrap();
170        mac.update(ciphertext_base64.as_bytes());
171        //cipher.encrypt(buffer, pos)
172        BSO {
173            id: object.id().to_string(),
174            payload: Payload {
175                iv: base64::encode(iv),
176                ciphertext: ciphertext_base64,
177                hmac: hex::encode(mac.finalize().into_bytes()),
178            },
179        }
180    }
181
182    fn decrypt_payload(&self, key: &[u8], hmac_key: &[u8]) -> Vec<u8> {
183        let payload = &self.payload;
184
185        let cipher =
186            Aes256Cbc::new_from_slices(key, &base64::decode(&payload.iv).unwrap()).unwrap();
187        let mut ciphertext = base64::decode(&payload.ciphertext).unwrap();
188        let len = cipher.decrypt(&mut ciphertext).unwrap().len();
189
190        let mut mac_verifier = Hmac::<Sha256>::new_from_slice(hmac_key).unwrap();
191        mac_verifier.update(payload.ciphertext.as_bytes());
192        mac_verifier
193            .verify(&hex::decode(&payload.hmac).unwrap())
194            .unwrap();
195
196        ciphertext.truncate(len);
197        ciphertext
198    }
199}
200
201impl<'a, 'b, 'c> AccountLoginRequest<'a, 'b, 'c> {
202    fn new(email: &'a str, auth_pw: &'a str, verification: Option<Verification<'b>>) -> Self {
203        let verification_method = verification
204            .as_ref()
205            .map(|verification| match verification {
206                Verification::EmailCaptcha(_) => "email-captcha",
207            });
208        let unblock_code = verification.map(|verification| match verification {
209            Verification::EmailCaptcha(code) => code,
210        });
211
212        Self {
213            email,
214            auth_pw,
215            unblock_code,
216            verification_method,
217            // Reason is required so it doesn't ask for an unblock code
218            reason: "login",
219        }
220    }
221}
222
223#[derive(Deserialize, Debug, Clone)]
224#[serde(rename_all = "camelCase")]
225struct AccountLoginResponse {
226    uid: String,
227    session_token: String,
228    key_fetch_token: String,
229    verification_method: Option<String>,
230    verified: bool,
231}
232
233#[derive(Deserialize, Debug, Clone)]
234#[serde(rename_all = "camelCase")]
235struct BadRequestError {
236    code: u16,
237    errno: u16,
238    message: String,
239    verification_method: Option<String>,
240    verification_reason: Option<String>,
241}
242
243#[derive(Serialize)]
244struct SendUnblockCodeRequest<'a> {
245    email: &'a str,
246}
247
248#[derive(Serialize)]
249struct PublicKey<'a> {
250    algorithm: &'a str,
251    n: &'a str,
252    e: &'a str,
253}
254
255#[derive(Serialize)]
256#[serde(rename_all = "camelCase")]
257struct CertificateSignRequest<'a> {
258    public_key: PublicKey<'a>,
259    duration: u64,
260}
261
262#[derive(Deserialize)]
263struct CertificateSignResponse {
264    cert: String,
265}
266
267#[derive(Deserialize)]
268struct AccountKeysResponse {
269    bundle: String,
270}
271
272pub struct SyncClient {
273    http_client: reqwest::Client,
274    api_endpoint: String,
275    sync_server_credentials: hawk::Credentials,
276    key_bundle: [u8; 64],
277}
278
279struct FxaClient {
280    client: reqwest::Client,
281    base_uri: String,
282}
283
284enum Verification<'a> {
285    EmailCaptcha(&'a str),
286}
287
288fn hawk_authenticate(request: &mut Request, credentials: &hawk::Credentials) {
289    let method = request.method().clone();
290    let url = request.url().clone();
291    let mut hawk_request_builder = hawk::RequestBuilder::from_url(method.as_str(), &url).unwrap();
292    let payload_hash;
293    if let Some(body) = request.body() {
294        payload_hash = hawk::PayloadHasher::hash(
295            request.headers().get("Content-Type").unwrap().as_bytes(),
296            hawk::SHA256,
297            body.as_bytes().unwrap(),
298        )
299        .unwrap();
300        hawk_request_builder = hawk_request_builder.hash(&payload_hash[..]);
301    }
302
303    let hawk_request = hawk_request_builder.request();
304    assert!(request
305        .headers_mut()
306        .insert(
307            header::AUTHORIZATION,
308            format!("Hawk {}", hawk_request.make_header(credentials).unwrap())
309                .parse()
310                .unwrap(),
311        )
312        .is_none());
313}
314
315impl FxaClient {
316    fn new() -> Self {
317        Self {
318            client: reqwest::Client::builder()
319                .user_agent("reqwest (pass-fxa)")
320                //.proxy(reqwest::Proxy::all("http://localhost:8080").unwrap())
321                //.danger_accept_invalid_certs(true)
322                .build()
323                .unwrap(),
324            // TODO: allow user to choose FxA server
325            base_uri: "https://api.accounts.firefox.com/v1".to_string(),
326        }
327    }
328
329    // TODO: move this crypto stuff somewhere else
330    async fn get_sync_client(self, email: &str, password: &str) -> SyncClient {
331        let email_salt = kwe("quickStretch", email);
332        let mut quick_stretched_pw = [0u8; 32];
333        pbkdf2::pbkdf2::<Hmac<Sha256>>(
334            password.as_bytes(),
335            email_salt.as_bytes(),
336            1000,
337            &mut quick_stretched_pw,
338        );
339        let mut auth_pw = [0u8; 32];
340        let quick_stretched_pw_hdkf = Hkdf::<Sha256>::new(None, &quick_stretched_pw);
341        quick_stretched_pw_hdkf
342            .expand(kw("authPW").as_bytes(), &mut auth_pw)
343            .unwrap();
344
345        let mut unwrap_b_key = [0u8; 32];
346        quick_stretched_pw_hdkf
347            .expand(kw("unwrapBkey").as_bytes(), &mut unwrap_b_key)
348            .unwrap();
349
350        let auth_pwd_hex = hex::encode(auth_pw);
351
352        let account_login_response = match self.account_login(email, &auth_pwd_hex, None).await {
353            Ok(account_login_response) => account_login_response,
354            Err(bad_request_error) => {
355                match bad_request_error.errno {
356                    125 => {
357                        assert!(bad_request_error.verification_method.is_some());
358                        self.account_login_send_unblock_code(email).await;
359                        print!("A verification code sent to {}: ", email);
360                        io::stdout().flush().unwrap();
361                        let mut unblock_code = String::new();
362                        std::io::stdin().read_line(&mut unblock_code).unwrap();
363                        self.account_login(
364                            email,
365                            &auth_pwd_hex,
366                            Some(Verification::EmailCaptcha(unblock_code.trim())),
367                        )
368                        // TODO move error handling into the api code instead.
369                        .await
370                        .unwrap_or_else(|bad_request_error| match bad_request_error.errno {
371                            127 => {
372                                println!("{}", bad_request_error.message);
373                                panic!();
374                            }
375                            _ => unimplemented!(),
376                        })
377                    }
378                    127 => {
379                        // Cannot have "Invalid unblock code" when no unblock code is given
380                        unreachable!();
381                    }
382                    _ => {
383                        dbg!(bad_request_error);
384                        unimplemented!();
385                    }
386                }
387            }
388        };
389
390        if let Some(ref verification_method) = account_login_response.verification_method {
391            match verification_method.as_str() {
392                "email" => println!("Please confirm sign-in by email at {}", email),
393                _ => unimplemented!(),
394            }
395        }
396
397        let mut derived_key_fetch_token = [0u8; 96];
398        Hkdf::<Sha256>::new(
399            None,
400            &hex::decode(account_login_response.key_fetch_token).unwrap(),
401        )
402        .expand(kw("keyFetchToken").as_bytes(), &mut derived_key_fetch_token)
403        .unwrap();
404
405        let token_id = &derived_key_fetch_token[0..32];
406        let req_hmac_key = &derived_key_fetch_token[32..64];
407        let key_request_key = &derived_key_fetch_token[64..96];
408
409        let hawk_credentials = hawk::Credentials {
410            id: hex::encode(token_id),
411            key: hawk::Key::new(req_hmac_key, hawk::DigestAlgorithm::Sha256).unwrap(),
412        };
413
414        let bundle = hex::decode(loop {
415            match self.account_keys(&hawk_credentials).await {
416                Ok(account_keys) => break account_keys.bundle,
417                Err(_) => {
418                    if account_login_response.verification_method.is_none() {
419                        unimplemented!()
420                    } else {
421                        sleep(Duration::from_millis(500)).await
422                    }
423                }
424            }
425        })
426        .unwrap();
427        let ciphertext = &bundle[0..64];
428        let mac = &bundle[64..96];
429
430        let mut derived_from_key_request_key = [0u8; 96];
431        Hkdf::<Sha256>::new(None, key_request_key)
432            .expand(
433                kw("account/keys").as_bytes(),
434                &mut derived_from_key_request_key,
435            )
436            .unwrap();
437
438        let mut mac_verifer =
439            Hmac::<Sha256>::new_from_slice(&derived_from_key_request_key[0..32]).unwrap();
440        mac_verifer.update(ciphertext);
441        mac_verifer
442            .verify(mac)
443            .expect("!!! CRYPTOGRAPHY ERROR SPOOFING IS BEING ATTEMPTED !!!");
444
445        xor(&mut derived_from_key_request_key[32..96], ciphertext);
446        xor(&mut derived_from_key_request_key[64..96], &unwrap_b_key);
447
448        let key_b = &derived_from_key_request_key[64..96];
449
450        let fxa_client_state = hex::encode(&Sha256::new().chain(&key_b).finalize()[0..16]);
451        // TODO: this can be done concurrently with the previous request
452        let browserid_assertion = &self
453            .get_browserid_assertion(&account_login_response.session_token)
454            .await;
455
456        let sync_server = self
457            .sync_server_tokens(&fxa_client_state, &browserid_assertion)
458            .await;
459        let sync_server_credentials = hawk::Credentials {
460            id: sync_server.id,
461            key: hawk::Key::new(sync_server.key.as_bytes(), hawk::DigestAlgorithm::Sha256).unwrap(),
462        };
463
464        let mut sync_key_bundle = [0u8; 64];
465        Hkdf::<Sha256>::new(None, key_b)
466            .expand(kw("oldsync").as_bytes(), &mut sync_key_bundle)
467            .unwrap();
468
469        SyncClient::from_sync_key_bundle(
470            self.client,
471            sync_server.api_endpoint,
472            sync_server_credentials,
473            sync_key_bundle,
474        )
475        .await
476    }
477
478    async fn account_login(
479        &self,
480        email: &str,
481        auth_pw: &str,
482        verification: Option<Verification<'_>>,
483    ) -> Result<AccountLoginResponse, BadRequestError> {
484        let response = self
485            .client
486            .post(format!("{}/account/login?keys=true", self.base_uri))
487            .json(&AccountLoginRequest::new(email, auth_pw, verification))
488            .send()
489            .await
490            .unwrap();
491        if response.status() == StatusCode::BAD_REQUEST {
492            Err(response.json::<BadRequestError>().await.unwrap())
493        } else {
494            Ok(response.json::<AccountLoginResponse>().await.unwrap())
495        }
496    }
497
498    // TODO no error handling. It is not possible to capture error from the calling code, as this
499    // panics.
500    async fn account_login_send_unblock_code(&self, email: &str) {
501        let response = self
502            .client
503            .post(format!("{}/account/login/send_unblock_code", self.base_uri))
504            .json(&SendUnblockCodeRequest { email })
505            .send()
506            .await
507            .unwrap();
508        match response.status() {
509            StatusCode::OK => {}
510            StatusCode::TOO_MANY_REQUESTS => {
511                println!(
512                    "Too many requests! Try again in {} seconds.",
513                    std::str::from_utf8(response.headers().get("retry-after").unwrap().as_bytes())
514                        .unwrap()
515                );
516                // TODO proper exit
517                panic!("TODO proper ending of stuff");
518            }
519            StatusCode::BAD_REQUEST => {}
520            _ => {
521                dbg!(&response);
522                dbg!(response.bytes().await.unwrap());
523                unimplemented!("An unexpected error occured.");
524            }
525        }
526    }
527
528    async fn account_keys(
529        &self,
530        credentials: &hawk::Credentials,
531    ) -> Result<AccountKeysResponse, reqwest::Error> {
532        let mut request = self
533            .client
534            .get(format!("{}/account/keys", self.base_uri))
535            .build()
536            .unwrap();
537        hawk_authenticate(&mut request, credentials);
538        self.client.execute(request).await.unwrap().json().await
539    }
540
541    async fn certificate_sign(
542        &self,
543        n: &str,
544        e: &str,
545        credentials: &hawk::Credentials,
546    ) -> (CertificateSignResponse, u64) {
547        let url = Url::parse(&format!("{}/certificate/sign", self.base_uri)).unwrap();
548        let mut request = self
549            .client
550            .post(url.clone())
551            .json(&CertificateSignRequest {
552                public_key: PublicKey {
553                    algorithm: "RS",
554                    n,
555                    e,
556                },
557                // Value in milliseconds
558                duration: DURATION * 1000,
559            })
560            .build()
561            .unwrap();
562        hawk_authenticate(&mut request, credentials);
563        let response = self.client.execute(request).await.unwrap();
564        let server_time = response
565            .headers()
566            .get("timestamp")
567            .unwrap()
568            .to_str()
569            .unwrap()
570            .parse()
571            .unwrap();
572
573        (response.json().await.unwrap(), server_time)
574    }
575
576    async fn sync_server_tokens(
577        &self,
578        client_state: &str,
579        browserid_assertion: &str,
580    ) -> SyncServerToken {
581        self.client
582            .get("https://token.services.mozilla.com/1.0/sync/1.5")
583            .header(
584                header::AUTHORIZATION,
585                format!("BrowserID {}", browserid_assertion),
586            )
587            .header("X-Client-State", client_state)
588            .send()
589            .await
590            .unwrap()
591            .json()
592            .await
593            .unwrap()
594    }
595
596    async fn _info_collections(&self, sync_server_endpoint: &str, credentials: &hawk::Credentials) {
597        let mut request = self
598            .client
599            .get(format!("{}/info/collections", sync_server_endpoint))
600            .build()
601            .unwrap();
602        hawk_authenticate(&mut request, credentials);
603        self.client.execute(request).await.unwrap();
604    }
605
606    async fn get_browserid_assertion(&self, session_token: &str) -> String {
607        let mut derived_from_session_token = [0u8; 64];
608        println!("Generating RSA Private Key. This may take a while.");
609        let rsa_private_key = RSAPrivateKey::new(&mut OsRng, 2048).unwrap();
610        Hkdf::<Sha256>::new(None, &hex::decode(session_token).unwrap())
611            .expand(
612                kw("sessionToken").as_bytes(),
613                &mut derived_from_session_token,
614            )
615            .unwrap();
616        let (certificate, server_time) = self
617            .certificate_sign(
618                &rsa_private_key.n().to_str_radix(10),
619                &rsa_private_key.e().to_str_radix(10),
620                &hawk::Credentials {
621                    id: hex::encode(&derived_from_session_token[0..32]),
622                    key: hawk::Key::new(
623                        &derived_from_session_token[32..64],
624                        hawk::DigestAlgorithm::Sha256,
625                    )
626                    .unwrap(),
627                },
628            )
629            .await;
630
631        let signed_data = format!(
632            "{}.{}",
633            base64::encode_config("{\"alg\": \"RS256\"}", base64::URL_SAFE_NO_PAD),
634            base64::encode_config(
635                &serde_json::to_string(&Assertion {
636                    exp: (server_time + DURATION) * 1000,
637                    aud: "https://token.services.mozilla.com/"
638                })
639                .unwrap(),
640                base64::URL_SAFE_NO_PAD
641            ),
642        );
643
644        let assertion = format!(
645            "{}.{}",
646            &signed_data,
647            base64::encode_config(
648                rsa_private_key
649                    .sign(
650                        PaddingScheme::PKCS1v15Sign {
651                            hash: Some(Hash::SHA2_256),
652                        },
653                        &Sha256::new().chain(&signed_data).finalize(),
654                    )
655                    .unwrap(),
656                base64::URL_SAFE_NO_PAD,
657            )
658        );
659
660        format!("{}~{}", certificate.cert, assertion)
661    }
662}
663
664#[derive(Deserialize)]
665struct BatchCollectionResponse {
666    success: Vec<String>,
667    // FIXME: These are individual attributes
668    failed: HashMap<String, String>,
669    batch: Option<String>,
670}
671
672impl SyncClient {
673    pub async fn new(email: &str, password: &str) -> Self {
674        FxaClient::new().get_sync_client(email, password).await
675    }
676
677    async fn from_sync_key_bundle(
678        http_client: reqwest::Client,
679        api_endpoint: String,
680        sync_server_credentials: hawk::Credentials,
681        sync_key_bundle: [u8; 64],
682    ) -> Self {
683        let sync = Self {
684            http_client,
685            api_endpoint,
686            sync_server_credentials,
687            key_bundle: sync_key_bundle,
688        };
689
690        let plaintext: CryptoKeyRecord = sync.get_storage_object("crypto/keys").await;
691        let mut bulk_key_bundle = [0u8; 64];
692        base64::decode_config_slice(
693            &plaintext.default[0],
694            base64::STANDARD,
695            &mut bulk_key_bundle,
696        )
697        .unwrap();
698        base64::decode_config_slice(
699            &plaintext.default[1],
700            base64::STANDARD,
701            &mut bulk_key_bundle[32..64],
702        )
703        .unwrap();
704
705        SyncClient {
706            key_bundle: bulk_key_bundle,
707            ..sync
708        }
709    }
710
711    async fn hawk_execute(
712        &self,
713        mut request: Request,
714    ) -> Result<reqwest::Response, reqwest::Error> {
715        hawk_authenticate(&mut request, &self.sync_server_credentials);
716        self.http_client.execute(request).await
717    }
718
719    async fn get_storage_object<T>(&self, object: impl AsRef<str>) -> T
720    where
721        T: de::DeserializeOwned,
722    {
723        let decrypted_payload = self
724            .hawk_execute(
725                self.http_client
726                    .get(format!("{}/storage/{}", self.api_endpoint, object.as_ref()))
727                    .build()
728                    .unwrap(),
729            )
730            .await
731            .unwrap()
732            .json::<BSO>()
733            .await
734            .unwrap()
735            .decrypt_payload(&self.key_bundle[0..32], &self.key_bundle[32..64]);
736        serde_json::from_slice(&decrypted_payload).unwrap()
737    }
738
739    pub async fn get_collection(&self, collection: &str) -> Vec<String> {
740        self.hawk_execute(
741            self.http_client
742                .get(format!("{}/storage/{}", self.api_endpoint, collection))
743                .build()
744                .unwrap(),
745        )
746        .await
747        .unwrap()
748        .json()
749        .await
750        .unwrap()
751    }
752
753    pub async fn get_logins(&self) -> Vec<Login> {
754        let mut passwords = Vec::new();
755        let password_bsos = self
756            .get_collection("passwords")
757            .await
758            .iter()
759            .map(|password_id| self.get_storage_object(format!("passwords/{}", password_id)))
760            .collect::<FuturesUnordered<_>>();
761        let passwords_len = password_bsos.len();
762        let mut password_bsos_enumerate = password_bsos.enumerate();
763        let mut stdout = io::stdout();
764        eprint!("[0/{}] Downloading passwords", passwords_len);
765        stdout.flush().unwrap();
766        while let Some((i, password_bso)) = password_bsos_enumerate.next().await {
767            if let PasswordBSORecord::Password(password) = password_bso {
768                passwords.push(password);
769            }
770            eprint!("\r[{}/{}] Downloading passwords", i + 1, passwords_len);
771            stdout.flush().unwrap();
772        }
773        eprintln!();
774        passwords
775    }
776
777    async fn post_collection(
778        &self,
779        objects: &[impl Serialize + BsoObject],
780        batch: Option<&str>,
781        commit: bool,
782    ) -> BatchCollectionResponse {
783        let mut query = Vec::new();
784        if commit {
785            query.push(("commit", "true"));
786        }
787        if let Some(batch) = batch {
788            query.push(("batch", batch));
789        }
790        self.hawk_execute(
791            self.http_client
792                .post(format!("{}/storage/passwords", self.api_endpoint))
793                .json(
794                    &objects
795                        .iter()
796                        .map(|login| {
797                            BSO::from_object(
798                                login,
799                                &self.key_bundle[0..32],
800                                &self.key_bundle[32..64],
801                            )
802                        })
803                        .collect::<Vec<_>>(),
804                )
805                .query(&query)
806                .build()
807                .unwrap(),
808        )
809        .await
810        .unwrap()
811        .json()
812        .await
813        .unwrap()
814    }
815
816    async fn upload_collection(&self, objects: &[impl BsoObject + Serialize]) {
817        if objects.len() > BATCH_SIZE {
818            let mut chunks = objects.chunks(BATCH_SIZE).enumerate();
819            let number_of_chunks = chunks.len();
820            let batch_id = self
821                .post_collection(chunks.next().unwrap().1, Some("true"), false)
822                .await
823                .batch
824                .unwrap();
825            for (i, chunk) in chunks {
826                let commit = i == number_of_chunks - 1;
827                // TODO deal with the failed BSOs, to retry them
828                self.post_collection(chunk, Some(&batch_id), commit).await;
829            }
830        } else {
831            self.post_collection(objects, None, false).await;
832        }
833    }
834
835    pub async fn put_logins(&self, logins: &[Login]) {
836        self.upload_collection(logins).await;
837    }
838
839    pub async fn delete_objects(self, ids: &[&str]) {
840        self.upload_collection(
841            &ids.iter()
842                .map(|id| Deleted {
843                    id: id.to_string(),
844                    deleted: true,
845                })
846                .collect::<Vec<_>>(),
847        )
848        .await;
849    }
850
851    pub async fn delete_ids(&self, collection: &str, ids: &[&str]) {
852        for chunk in ids.chunks(BATCH_SIZE) {
853            self.hawk_execute(
854                self.http_client
855                    .delete(format!("{}/storage/{}", self.api_endpoint, collection))
856                    .query(&[("ids", &chunk.join(","))])
857                    .build()
858                    .unwrap(),
859            )
860            .await
861            .unwrap();
862        }
863    }
864}
865
866fn xor(a: &mut [u8], b: &[u8]) {
867    for (x, y) in a.iter_mut().zip(b.iter()) {
868        *x ^= *y;
869    }
870}
871
872fn kwe(name: &str, email: &str) -> String {
873    format!("{}:{}", kw(name), email)
874}
875
876fn kw(name: &str) -> String {
877    format!("identity.mozilla.com/picl/v1/{}", name)
878}
879
880#[derive(Serialize)]
881struct Assertion<'a> {
882    exp: u64,
883    aud: &'a str,
884}
885
886fn generate_iv() -> [u8; 16] {
887    let mut iv = [0u8; 16];
888    OsRng.fill_bytes(&mut iv);
889    return iv;
890}
891#[cfg(test)]
892mod tests {
893    use super::*;
894
895    #[test]
896    fn serialize_origin_test() {
897        assert_eq!(
898            &serde_json::to_string(&Login {
899                id: "CZtAJxrSHbA0".to_string(),
900                ..Login::new(
901                    "username",
902                    "password",
903                    Url::parse("https://github.com").unwrap()
904                )
905            })
906            .unwrap(),
907            r#"{"id":"CZtAJxrSHbA0","hostname":"https://github.com","formSubmitURL":"","httpRealm":null,"username":"username","password":"password","usernameField":"","passwordField":""}"#
908        );
909    }
910
911    // TODO: add test for only containing URL safe characters
912    #[test]
913    fn generate_bso_id_test1() {
914        assert_eq!(12, generate_bso_id().len());
915    }
916
917    #[test]
918    fn parse_bso() {
919        serde_json::from_str::<BSO>(
920            r#"
921{
922  "id": "ybhmIXr2Vj9Y",
923  "modified": 1616761977.69,
924  "payload": "{\"IV\":\"oZU7SOKC/bON6y7dpjl5OQ==\",\"hmac\":\"eebb747b790794d560b29897e9f1c4da9d3d2139bec9c64b526bd3cb0f096b46\",\"ciphertext\":\"V+qB+4Lb+6AeDmIKt/0GpnzWVC8eDrQJkzQfWhb1OsAHH4vqQaA2ZJVxuB8TmUA3xGVUxsUbIun9yc0B3ZM4KwaicqXdtWStlh+JEM9yJGAduDwwHZvPRINu1gEki5t6tw19Bira63RxyGyMj2lqpbGq0IIWOAKrKiPQFVteYLkjR9dRM+R6vJZB/5TC3eoMB75drHTNSB4UOxiUwmqmx6gZbXss+73Au+bs63ODx0A8wcDRyxThpQOKWVOhPvmSQLpWRzStNBI0z20owEg03QKevA6xheZ+vtOqnYcN7O0+5xBVTM3Xg2ykqVXrayeYnHig9KVRAucgH/ImBzenJw2/dTZeJKszLBuMxQ4vLSiDF8lSp6Pae7xLeDUdE+AbVCirOJX+Ren5XF17v9XhDV8C4Okn3gCbFKawu8cacvf5cna/Ezhsc1HuMu2HtgTA\"}",
925  "sortindex": 1
926}
927            "#,
928        ).unwrap();
929    }
930
931    #[test]
932    fn deserialize_json_password_test() {
933        let json = r#"
934{
935  "id": "{7f3db3a7-ef2d-0446-aad0-049f1b0ff0fa}",
936  "hostname": "https://www.reddit.com",
937  "formSubmitURL": "",
938  "httpRealm": null,
939  "username": "asdf",
940  "password": "asdf",
941  "usernameField": "",
942  "passwordField": "",
943  "timeCreated": 1626895557678,
944  "timePasswordChanged": 1626895557678
945}
946        "#;
947        serde_json::from_str::<PasswordBSORecord>(json).unwrap();
948    }
949
950    #[test]
951    fn deserialize_nested() {
952        #[derive(Deserialize)]
953        struct B {
954            c: String,
955            d: u32,
956        }
957
958        #[derive(Deserialize)]
959        struct Test {
960            a: String,
961            #[serde(with = "serde_with::json::nested")]
962            b: B,
963        }
964
965        let object: Test = serde_json::from_str(
966            r#"
967            {
968                "a": "string",
969                "b": "{\"c\":\"c_string\",\"d\":1234}"
970            }
971        "#,
972        )
973        .unwrap();
974    }
975}