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 username_field: String,
48 password_field: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 time_last_used: Option<u64>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 time_created: Option<u64>,
55 #[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 #[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 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: "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 .build()
323 .unwrap(),
324 base_uri: "https://api.accounts.firefox.com/v1".to_string(),
326 }
327 }
328
329 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 .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 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 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 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 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 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 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 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 #[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}