1use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6
7use anyhow::{Context as _, Result, bail, ensure, format_err};
8use futures::TryStreamExt;
9use futures_lite::FutureExt;
10use pin_project::pin_project;
11
12use tokio::fs::{self, File};
13use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
14use tokio_tar::Archive;
15
16use crate::blob::BlobDirContents;
17use crate::chat::delete_and_reset_all_device_msgs;
18use crate::config::Config;
19use crate::context::Context;
20use crate::e2ee;
21use crate::events::EventType;
22use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
23use crate::log::{LogExt, error, info, warn};
24use crate::pgp;
25use crate::qr::DCBACKUP_VERSION;
26use crate::sql;
27use crate::tools::{
28 TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file,
29};
30
31mod key_transfer;
32mod transfer;
33
34use ::pgp::types::KeyDetails;
35pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
36pub use transfer::{BackupProvider, get_backup};
37
38const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
40pub(crate) const BLOBS_BACKUP_NAME: &str = "blobs_backup";
41
42#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
44#[repr(u32)]
45pub enum ImexMode {
46 ExportSelfKeys = 1,
51
52 ImportSelfKeys = 2,
57
58 ExportBackup = 11,
63
64 ImportBackup = 12,
68}
69
70pub async fn imex(
85 context: &Context,
86 what: ImexMode,
87 path: &Path,
88 passphrase: Option<String>,
89) -> Result<()> {
90 let cancel = context.alloc_ongoing().await?;
91
92 let res = {
93 let _guard = context.scheduler.pause(context).await?;
94 imex_inner(context, what, path, passphrase)
95 .race(async {
96 cancel.recv().await.ok();
97 Err(format_err!("canceled"))
98 })
99 .await
100 };
101 context.free_ongoing().await;
102
103 if let Err(err) = res.as_ref() {
104 error!(context, "IMEX failed to complete: {:#}", err);
106 context.emit_event(EventType::ImexProgress(0));
107 } else {
108 info!(context, "IMEX successfully completed");
109 context.emit_event(EventType::ImexProgress(1000));
110 }
111
112 res
113}
114
115pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
117 let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
118 let mut newest_backup_name = "".to_string();
119 let mut newest_backup_path: Option<PathBuf> = None;
120
121 while let Ok(Some(dirent)) = dir_iter.next_entry().await {
122 let path = dirent.path();
123 let name = dirent.file_name();
124 let name: String = name.to_string_lossy().into();
125 if name.starts_with("delta-chat")
126 && name.ends_with(".tar")
127 && (newest_backup_name.is_empty() || name > newest_backup_name)
128 {
129 newest_backup_path = Some(path);
132 newest_backup_name = name;
133 }
134 }
135
136 match newest_backup_path {
137 Some(path) => Ok(path.to_string_lossy().into_owned()),
138 None => bail!("no backup found in {}", dir_name.display()),
139 }
140}
141
142async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
143 let private_key = SignedSecretKey::from_asc(armored)?;
144 let public_key = private_key.split_public_key()?;
145
146 let keypair = pgp::KeyPair {
147 public: public_key,
148 secret: private_key,
149 };
150 key::store_self_keypair(context, &keypair).await?;
151
152 info!(
153 context,
154 "stored self key: {:?}",
155 keypair.secret.public_key().key_id()
156 );
157 Ok(())
158}
159
160async fn imex_inner(
161 context: &Context,
162 what: ImexMode,
163 path: &Path,
164 passphrase: Option<String>,
165) -> Result<()> {
166 info!(
167 context,
168 "{} path: {}",
169 match what {
170 ImexMode::ExportSelfKeys | ImexMode::ExportBackup => "Export",
171 ImexMode::ImportSelfKeys | ImexMode::ImportBackup => "Import",
172 },
173 path.display()
174 );
175 ensure!(context.sql.is_open().await, "Database not opened.");
176 context.emit_event(EventType::ImexProgress(1));
177
178 if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
179 e2ee::ensure_secret_key_exists(context)
181 .await
182 .context("Cannot create private key or private key not available")?;
183
184 create_folder(context, path).await?;
185 }
186
187 match what {
188 ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
189 ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
190
191 ImexMode::ExportBackup => {
192 export_backup(context, path, passphrase.unwrap_or_default()).await
193 }
194 ImexMode::ImportBackup => {
195 import_backup(context, path, passphrase.unwrap_or_default()).await
196 }
197 }
198}
199
200async fn import_backup(
207 context: &Context,
208 backup_to_import: &Path,
209 passphrase: String,
210) -> Result<()> {
211 ensure!(
212 !context.is_configured().await?,
213 "Cannot import backups to accounts in use."
214 );
215 ensure!(
216 !context.scheduler.is_running().await,
217 "cannot import backup, IO is running"
218 );
219
220 let backup_file = File::open(backup_to_import).await?;
221 let file_size = backup_file.metadata().await?.len();
222 info!(
223 context,
224 "Import \"{}\" ({} bytes) to \"{}\".",
225 backup_to_import.display(),
226 file_size,
227 context.get_dbfile().display()
228 );
229
230 import_backup_stream(context, backup_file, file_size, passphrase).await?;
231 Ok(())
232}
233
234pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
248 context: &Context,
249 backup_file: R,
250 file_size: u64,
251 passphrase: String,
252) -> Result<()> {
253 import_backup_stream_inner(context, backup_file, file_size, passphrase)
254 .await
255 .0
256}
257
258#[pin_project]
260struct ProgressReader<R> {
261 #[pin]
263 inner: R,
264
265 read: usize,
267
268 file_size: usize,
271
272 last_progress: usize,
274
275 context: Context,
277}
278
279impl<R> ProgressReader<R> {
280 fn new(r: R, context: Context, file_size: u64) -> Self {
281 Self {
282 inner: r,
283 read: 0,
284 file_size: file_size as usize,
285 last_progress: 1,
286 context,
287 }
288 }
289}
290
291impl<R> AsyncRead for ProgressReader<R>
292where
293 R: AsyncRead,
294{
295 fn poll_read(
296 self: Pin<&mut Self>,
297 cx: &mut std::task::Context<'_>,
298 buf: &mut ReadBuf<'_>,
299 ) -> std::task::Poll<std::io::Result<()>> {
300 let this = self.project();
301 let before = buf.filled().len();
302 let res = this.inner.poll_read(cx, buf);
303 if let std::task::Poll::Ready(Ok(())) = res {
304 *this.read = this.read.saturating_add(buf.filled().len() - before);
305
306 let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999);
307 if progress > *this.last_progress {
308 this.context.emit_event(EventType::ImexProgress(progress));
309 *this.last_progress = progress;
310 }
311 }
312 res
313 }
314}
315
316async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
317 context: &Context,
318 backup_file: R,
319 file_size: u64,
320 passphrase: String,
321) -> (Result<()>,) {
322 let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
323 let mut archive = Archive::new(backup_file);
324
325 let mut entries = match archive.entries() {
326 Ok(entries) => entries,
327 Err(e) => return (Err(e).context("Failed to get archive entries"),),
328 };
329 let mut blobs = Vec::new();
330 let mut res: Result<()> = loop {
331 let mut f = match entries.try_next().await {
332 Ok(Some(f)) => f,
333 Ok(None) => break Ok(()),
334 Err(e) => break Err(e).context("Failed to get next entry"),
335 };
336
337 let path = match f.path() {
338 Ok(path) => path.to_path_buf(),
339 Err(e) => break Err(e).context("Failed to get entry path"),
340 };
341 if let Err(e) = f.unpack_in(context.get_blobdir()).await {
342 break Err(e).context("Failed to unpack file");
343 }
344 if path.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
345 continue;
346 }
347 let from_path = context.get_blobdir().join(&path);
349 if from_path.is_file() {
350 if let Some(name) = from_path.file_name() {
351 let to_path = context.get_blobdir().join(name);
352 if let Err(e) = fs::rename(&from_path, &to_path).await {
353 blobs.push(from_path);
354 break Err(e).context("Failed to move file to blobdir");
355 }
356 blobs.push(to_path);
357 } else {
358 warn!(context, "No file name");
359 }
360 }
361 };
362 if res.is_err() {
363 for blob in blobs {
364 fs::remove_file(&blob).await.log_err(context).ok();
365 }
366 }
367
368 let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
369 if res.is_ok() {
370 res = context
371 .sql
372 .import(&unpacked_database, passphrase.clone())
373 .await
374 .context("cannot import unpacked database");
375 }
376 if res.is_ok() {
377 res = check_backup_version(context).await;
378 }
379 if res.is_ok() {
380 res = adjust_bcc_self(context).await;
381 }
382 fs::remove_file(unpacked_database)
383 .await
384 .context("cannot remove unpacked database")
385 .log_err(context)
386 .ok();
387 if res.is_ok() {
388 context.emit_event(EventType::ImexProgress(999));
389 res = context.sql.run_migrations(context).await;
390 context.emit_event(EventType::AccountsItemChanged);
391 }
392 if res.is_ok() {
393 delete_and_reset_all_device_msgs(context)
394 .await
395 .log_err(context)
396 .ok();
397 }
398 (res,)
399}
400
401fn get_next_backup_path(
409 folder: &Path,
410 addr: &str,
411 backup_time: i64,
412) -> Result<(PathBuf, PathBuf, PathBuf)> {
413 let folder = PathBuf::from(folder);
414 let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
415 .context("can't get next backup path")?
416 .format("delta-chat-backup-%Y-%m-%d")
418 .to_string();
419
420 for i in 0..64 {
422 let mut tempdbfile = folder.clone();
423 tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
424
425 let mut tempfile = folder.clone();
426 tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
427
428 let mut destfile = folder.clone();
429 destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
430
431 if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
432 return Ok((tempdbfile, tempfile, destfile));
433 }
434 }
435 bail!("could not create backup file, disk full?");
436}
437
438async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
442 let now = time();
444 let self_addr = context.get_primary_self_addr().await?;
445 let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
446 let temp_db_path = TempPathGuard::new(temp_db_path);
447 let temp_path = TempPathGuard::new(temp_path);
448
449 export_database(context, &temp_db_path, passphrase, now)
450 .await
451 .context("could not export database")?;
452
453 info!(
454 context,
455 "Backup '{}' to '{}'.",
456 context.get_dbfile().display(),
457 dest_path.display(),
458 );
459
460 let file = File::create(&temp_path).await?;
461 let blobdir = BlobDirContents::new(context).await?;
462
463 let mut file_size = 0;
464 file_size += temp_db_path.metadata()?.len();
465 for blob in blobdir.iter() {
466 file_size += blob.to_abs_path().metadata()?.len()
467 }
468
469 export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
470 .await
471 .context("Exporting backup to file failed")?;
472 fs::rename(temp_path, &dest_path).await?;
473 context.emit_event(EventType::ImexFileWritten(dest_path));
474 Ok(())
475}
476
477#[pin_project]
479struct ProgressWriter<W> {
480 #[pin]
482 inner: W,
483
484 written: usize,
486
487 file_size: usize,
490
491 last_progress: usize,
493
494 context: Context,
496}
497
498impl<W> ProgressWriter<W> {
499 fn new(w: W, context: Context, file_size: u64) -> Self {
500 Self {
501 inner: w,
502 written: 0,
503 file_size: file_size as usize,
504 last_progress: 1,
505 context,
506 }
507 }
508}
509
510impl<W> AsyncWrite for ProgressWriter<W>
511where
512 W: AsyncWrite,
513{
514 fn poll_write(
515 self: Pin<&mut Self>,
516 cx: &mut std::task::Context<'_>,
517 buf: &[u8],
518 ) -> std::task::Poll<Result<usize, std::io::Error>> {
519 let this = self.project();
520 let res = this.inner.poll_write(cx, buf);
521 if let std::task::Poll::Ready(Ok(written)) = res {
522 *this.written = this.written.saturating_add(written);
523
524 let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
525 if progress > *this.last_progress {
526 this.context.emit_event(EventType::ImexProgress(progress));
527 *this.last_progress = progress;
528 }
529 }
530 res
531 }
532
533 fn poll_flush(
534 self: Pin<&mut Self>,
535 cx: &mut std::task::Context<'_>,
536 ) -> std::task::Poll<Result<(), std::io::Error>> {
537 self.project().inner.poll_flush(cx)
538 }
539
540 fn poll_shutdown(
541 self: Pin<&mut Self>,
542 cx: &mut std::task::Context<'_>,
543 ) -> std::task::Poll<Result<(), std::io::Error>> {
544 self.project().inner.poll_shutdown(cx)
545 }
546}
547
548pub(crate) async fn export_backup_stream<'a, W>(
550 context: &'a Context,
551 temp_db_path: &Path,
552 blobdir: BlobDirContents<'a>,
553 writer: W,
554 file_size: u64,
555) -> Result<()>
556where
557 W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
558{
559 let writer = ProgressWriter::new(writer, context.clone(), file_size);
560 let mut builder = tokio_tar::Builder::new(writer);
561
562 builder
563 .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
564 .await?;
565
566 for blob in blobdir.iter() {
567 let mut file = File::open(blob.to_abs_path()).await?;
568 let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
569 builder.append_file(path_in_archive, &mut file).await?;
570 }
571
572 builder.finish().await?;
573 Ok(())
574}
575
576async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
578 let buf = read_file(context, path).await?;
579 let armored = std::string::String::from_utf8_lossy(&buf);
580 set_self_key(context, &armored).await?;
581 Ok(())
582}
583
584async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
594 let attr = tokio::fs::metadata(path).await?;
595
596 if attr.is_file() {
597 info!(
598 context,
599 "Importing secret key from {} as the default key.",
600 path.display()
601 );
602 import_secret_key(context, path).await?;
603 return Ok(());
604 }
605
606 let mut imported_cnt = 0;
607
608 let mut dir_handle = tokio::fs::read_dir(&path).await?;
609 while let Ok(Some(entry)) = dir_handle.next_entry().await {
610 let entry_fn = entry.file_name();
611 let name_f = entry_fn.to_string_lossy();
612 let path_plus_name = path.join(&entry_fn);
613 if let Some(suffix) = get_filesuffix_lc(&name_f) {
614 if suffix != "asc" {
615 continue;
616 }
617 } else {
618 continue;
619 };
620 info!(
621 context,
622 "Considering key file: {}.",
623 path_plus_name.display()
624 );
625
626 if let Err(err) = import_secret_key(context, &path_plus_name).await {
627 warn!(
628 context,
629 "Failed to import secret key from {}: {:#}.",
630 path_plus_name.display(),
631 err
632 );
633 continue;
634 }
635
636 imported_cnt += 1;
637 }
638 ensure!(
639 imported_cnt > 0,
640 "No private keys found in {}.",
641 path.display()
642 );
643 Ok(())
644}
645
646async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
647 let mut export_errors = 0;
648
649 let keys = context
650 .sql
651 .query_map(
652 "SELECT id, public_key, private_key, id=(SELECT value FROM config WHERE keyname='key_id') FROM keypairs;",
653 (),
654 |row| {
655 let id = row.get(0)?;
656 let public_key_blob: Vec<u8> = row.get(1)?;
657 let public_key = SignedPublicKey::from_slice(&public_key_blob);
658 let private_key_blob: Vec<u8> = row.get(2)?;
659 let private_key = SignedSecretKey::from_slice(&private_key_blob);
660 let is_default: i32 = row.get(3)?;
661
662 Ok((id, public_key, private_key, is_default))
663 },
664 |keys| {
665 keys.collect::<std::result::Result<Vec<_>, _>>()
666 .map_err(Into::into)
667 },
668 )
669 .await?;
670 let self_addr = context.get_primary_self_addr().await?;
671 for (id, public_key, private_key, is_default) in keys {
672 let id = Some(id).filter(|_| is_default == 0);
673
674 if let Ok(key) = public_key {
675 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
676 error!(context, "Failed to export public key: {:#}.", err);
677 export_errors += 1;
678 }
679 } else {
680 export_errors += 1;
681 }
682 if let Ok(key) = private_key {
683 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
684 error!(context, "Failed to export private key: {:#}.", err);
685 export_errors += 1;
686 }
687 } else {
688 export_errors += 1;
689 }
690 }
691
692 ensure!(export_errors == 0, "errors while exporting keys");
693 Ok(())
694}
695
696async fn export_key_to_asc_file<T>(
698 context: &Context,
699 dir: &Path,
700 addr: &str,
701 id: Option<i64>,
702 key: &T,
703) -> Result<String>
704where
705 T: DcKey,
706{
707 let file_name = {
708 let kind = match T::is_private() {
709 false => "public",
710 true => "private",
711 };
712 let id = id.map_or("default".into(), |i| i.to_string());
713 let fp = key.dc_fingerprint().hex();
714 format!("{kind}-key-{addr}-{id}-{fp}.asc")
715 };
716 let path = dir.join(&file_name);
717 info!(
718 context,
719 "Exporting key {:?} to {}.",
720 key.key_id(),
721 path.display()
722 );
723
724 delete_file(context, &path).await.ok();
726
727 let content = key.to_asc(None).into_bytes();
728 write_file(context, &path, &content)
729 .await
730 .with_context(|| format!("cannot write key to {}", path.display()))?;
731 context.emit_event(EventType::ImexFileWritten(path));
732 Ok(file_name)
733}
734
735async fn export_database(
742 context: &Context,
743 dest: &Path,
744 passphrase: String,
745 timestamp: i64,
746) -> Result<()> {
747 ensure!(
748 !context.scheduler.is_running().await,
749 "cannot export backup, IO is running"
750 );
751 let timestamp = timestamp.try_into().context("32-bit UNIX time overflow")?;
752
753 let dest = dest
755 .to_str()
756 .with_context(|| format!("path {} is not valid unicode", dest.display()))?;
757
758 adjust_bcc_self(context).await?;
759 context
760 .sql
761 .set_raw_config_int("backup_time", timestamp)
762 .await?;
763 context
764 .sql
765 .set_raw_config_int("backup_version", DCBACKUP_VERSION)
766 .await?;
767 sql::housekeeping(context).await.log_err(context).ok();
768 context
769 .sql
770 .call_write(|conn| {
771 conn.execute("VACUUM;", ())
772 .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
773 .ok();
774 conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
775 .context("failed to attach backup database")?;
776 let res = conn
777 .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
778 .context("failed to export to attached backup database");
779 conn.execute(
780 "UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
781 [],
782 )
783 .ok(); conn.execute("DETACH DATABASE backup", [])
785 .context("failed to detach backup database")?;
786 res?;
787 Ok(())
788 })
789 .await
790}
791
792async fn adjust_bcc_self(context: &Context) -> Result<()> {
798 if context.is_chatmail().await? && !context.config_exists(Config::BccSelf).await? {
799 context.set_config(Config::BccSelf, Some("1")).await?;
800 }
801 Ok(())
802}
803
804async fn check_backup_version(context: &Context) -> Result<()> {
805 let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
806 ensure!(
807 version <= DCBACKUP_VERSION,
808 "Backup too new, please update Delta Chat"
809 );
810 Ok(())
811}
812
813#[cfg(test)]
814mod tests {
815 use std::time::Duration;
816
817 use tokio::task;
818
819 use super::*;
820 use crate::config::Config;
821 use crate::test_utils::{TestContext, alice_keypair};
822
823 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
824 async fn test_export_public_key_to_asc_file() {
825 let context = TestContext::new().await;
826 let key = alice_keypair().public;
827 let blobdir = Path::new("$BLOBDIR");
828 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
829 .await
830 .unwrap();
831 assert!(filename.starts_with("public-key-a@b-default-"));
832 assert!(filename.ends_with(".asc"));
833 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
834 let filename = format!("{blobdir}/{filename}");
835 let bytes = tokio::fs::read(&filename).await.unwrap();
836
837 assert_eq!(bytes, key.to_asc(None).into_bytes());
838 }
839
840 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
841 async fn test_import_private_key_exported_to_asc_file() {
842 let context = TestContext::new().await;
843 let key = alice_keypair().secret;
844 let blobdir = Path::new("$BLOBDIR");
845 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
846 .await
847 .unwrap();
848 let fingerprint = filename
849 .strip_prefix("private-key-a@b-default-")
850 .unwrap()
851 .strip_suffix(".asc")
852 .unwrap();
853 assert_eq!(fingerprint, key.dc_fingerprint().hex());
854 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
855 let filename = format!("{blobdir}/{filename}");
856 let bytes = tokio::fs::read(&filename).await.unwrap();
857
858 assert_eq!(bytes, key.to_asc(None).into_bytes());
859
860 let alice = &TestContext::new().await;
861 if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
862 panic!("got error on import: {err:#}");
863 }
864 }
865
866 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
867 async fn test_export_and_import_key_from_dir() {
868 let export_dir = tempfile::tempdir().unwrap();
869
870 let context = TestContext::new_alice().await;
871 if let Err(err) = imex(
872 &context.ctx,
873 ImexMode::ExportSelfKeys,
874 export_dir.path(),
875 None,
876 )
877 .await
878 {
879 panic!("got error on export: {err:#}");
880 }
881
882 let context2 = TestContext::new().await;
883 if let Err(err) = imex(
884 &context2.ctx,
885 ImexMode::ImportSelfKeys,
886 export_dir.path(),
887 None,
888 )
889 .await
890 {
891 panic!("got error on import: {err:#}");
892 }
893 }
894
895 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
896 async fn test_import_second_key() -> Result<()> {
897 let alice = &TestContext::new_alice().await;
898 let chat = alice.create_chat(alice).await;
899 let sent = alice.send_text(chat.id, "Encrypted with old key").await;
900 let export_dir = tempfile::tempdir().unwrap();
901
902 let alice = &TestContext::new().await;
903 alice.configure_addr("alice@example.org").await;
904 imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
905
906 let alice = &TestContext::new_alice().await;
907 let old_key = key::load_self_secret_key(alice).await?;
908
909 assert!(
910 imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
911 .await
912 .is_err()
913 );
914
915 assert_eq!(key::load_self_secret_key(alice).await?, old_key);
918
919 assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
920
921 let msg = alice.recv_msg(&sent).await;
922 assert!(msg.get_showpadlock());
923 assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
924 assert_eq!(msg.get_text(), "Encrypted with old key");
925
926 Ok(())
927 }
928
929 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
930 async fn test_export_and_import_backup() -> Result<()> {
931 for set_verified_oneonone_chats in [true, false] {
932 let backup_dir = tempfile::tempdir().unwrap();
933
934 let context1 = TestContext::new_alice().await;
935 assert!(context1.is_configured().await?);
936 if set_verified_oneonone_chats {
937 context1
938 .set_config_bool(Config::VerifiedOneOnOneChats, true)
939 .await?;
940 }
941
942 let context2 = TestContext::new().await;
943 assert!(!context2.is_configured().await?);
944 assert!(has_backup(&context2, backup_dir.path()).await.is_err());
945
946 assert!(
948 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
949 .await
950 .is_ok()
951 );
952 let _event = context1
953 .evtracker
954 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
955 .await;
956
957 let backup = has_backup(&context2, backup_dir.path()).await?;
959
960 assert!(
962 imex(
963 &context2,
964 ImexMode::ImportBackup,
965 backup.as_ref(),
966 Some("foobar".to_string())
967 )
968 .await
969 .is_err()
970 );
971
972 assert!(
973 imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
974 .await
975 .is_ok()
976 );
977 let _event = context2
978 .evtracker
979 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
980 .await;
981
982 assert!(context2.is_configured().await?);
983 assert_eq!(
984 context2.get_config(Config::Addr).await?,
985 Some("alice@example.org".to_string())
986 );
987 assert_eq!(
988 context2
989 .get_config_bool(Config::VerifiedOneOnOneChats)
990 .await?,
991 false
992 );
993 assert_eq!(
994 context1
995 .get_config_bool(Config::VerifiedOneOnOneChats)
996 .await?,
997 set_verified_oneonone_chats
998 );
999 }
1000 Ok(())
1001 }
1002
1003 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1004 async fn test_export_import_chatmail_backup() -> Result<()> {
1005 let backup_dir = tempfile::tempdir().unwrap();
1006
1007 let context1 = &TestContext::new_alice().await;
1008
1009 assert_eq!(
1011 context1.get_config(Config::BccSelf).await?,
1012 Some("1".to_string())
1013 );
1014 assert_eq!(
1015 context1.get_config(Config::DeleteServerAfter).await?,
1016 Some("0".to_string())
1017 );
1018 context1.set_config_bool(Config::IsChatmail, true).await?;
1019 assert_eq!(
1020 context1.get_config(Config::BccSelf).await?,
1021 Some("0".to_string())
1022 );
1023 assert_eq!(
1024 context1.get_config(Config::DeleteServerAfter).await?,
1025 Some("1".to_string())
1026 );
1027
1028 assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
1029 imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1030 let _event = context1
1031 .evtracker
1032 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1033 .await;
1034
1035 let context2 = &TestContext::new().await;
1036 let backup = has_backup(context2, backup_dir.path()).await?;
1037 imex(context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
1038 let _event = context2
1039 .evtracker
1040 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1041 .await;
1042 assert!(context2.is_configured().await?);
1043 assert!(context2.is_chatmail().await?);
1044 for ctx in [context1, context2] {
1045 assert_eq!(
1046 ctx.get_config(Config::BccSelf).await?,
1047 Some("1".to_string())
1048 );
1049 assert_eq!(
1050 ctx.get_config(Config::DeleteServerAfter).await?,
1051 Some("0".to_string())
1052 );
1053 assert_eq!(ctx.get_config_delete_server_after().await?, None);
1054 }
1055 Ok(())
1056 }
1057
1058 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1062 async fn test_import_backup_reset_config_cache() -> Result<()> {
1063 let backup_dir = tempfile::tempdir()?;
1064 let context1 = TestContext::new_alice().await;
1065 let context2 = TestContext::new().await;
1066 assert!(!context2.is_configured().await?);
1067
1068 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1070
1071 let backup = has_backup(&context2, backup_dir.path()).await?;
1073 let context2_cloned = context2.clone();
1074 let handle = task::spawn(async move {
1075 imex(
1076 &context2_cloned,
1077 ImexMode::ImportBackup,
1078 backup.as_ref(),
1079 None,
1080 )
1081 .await
1082 .unwrap();
1083 });
1084
1085 while !handle.is_finished() {
1086 context2.is_configured().await.ok();
1089 tokio::time::sleep(Duration::from_micros(1)).await;
1090 }
1091
1092 assert!(context2.is_configured().await?);
1094
1095 Ok(())
1096 }
1097}