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, SignedSecretKey};
23use crate::log::{LogExt, warn};
24use crate::qr::DCBACKUP_VERSION;
25use crate::sql;
26use crate::tools::{
27 TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, usize_to_u64,
28 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, "{:#}", err);
106 warn!(context, "IMEX failed to complete: {:#}", err);
107 context.emit_event(EventType::ImexProgress(0));
108 } else {
109 info!(context, "IMEX successfully completed");
110 context.emit_event(EventType::ImexProgress(1000));
111 }
112
113 res
114}
115
116pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
118 let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
119 let mut newest_backup_name = "".to_string();
120 let mut newest_backup_path: Option<PathBuf> = None;
121
122 while let Ok(Some(dirent)) = dir_iter.next_entry().await {
123 let path = dirent.path();
124 let name = dirent.file_name();
125 let name: String = name.to_string_lossy().into();
126 if name.starts_with("delta-chat")
127 && name.ends_with(".tar")
128 && (newest_backup_name.is_empty() || name > newest_backup_name)
129 {
130 newest_backup_path = Some(path);
133 newest_backup_name = name;
134 }
135 }
136
137 match newest_backup_path {
138 Some(path) => Ok(path.to_string_lossy().into_owned()),
139 None => bail!("no backup found in {}", dir_name.display()),
140 }
141}
142
143async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
144 let secret_key = SignedSecretKey::from_asc(armored)?;
145 key::store_self_keypair(context, &secret_key).await?;
146
147 info!(
148 context,
149 "Stored self key: {:?}.",
150 secret_key.public_key().fingerprint()
151 );
152 Ok(())
153}
154
155async fn imex_inner(
156 context: &Context,
157 what: ImexMode,
158 path: &Path,
159 passphrase: Option<String>,
160) -> Result<()> {
161 info!(
162 context,
163 "{} path: {}",
164 match what {
165 ImexMode::ExportSelfKeys | ImexMode::ExportBackup => "Export",
166 ImexMode::ImportSelfKeys | ImexMode::ImportBackup => "Import",
167 },
168 path.display()
169 );
170 ensure!(context.sql.is_open().await, "Database not opened.");
171 context.emit_event(EventType::ImexProgress(1));
172
173 if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
174 e2ee::ensure_secret_key_exists(context)
176 .await
177 .context("Cannot create private key or private key not available")?;
178
179 create_folder(context, path).await?;
180 }
181
182 match what {
183 ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
184 ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
185
186 ImexMode::ExportBackup => {
187 export_backup(context, path, passphrase.unwrap_or_default()).await
188 }
189 ImexMode::ImportBackup => {
190 import_backup(context, path, passphrase.unwrap_or_default()).await
191 }
192 }
193}
194
195async fn import_backup(
202 context: &Context,
203 backup_to_import: &Path,
204 passphrase: String,
205) -> Result<()> {
206 let backup_file = File::open(backup_to_import).await?;
207 let file_size = backup_file.metadata().await?.len();
208 info!(
209 context,
210 "Import \"{}\" ({} bytes) to \"{}\".",
211 backup_to_import.display(),
212 file_size,
213 context.get_dbfile().display()
214 );
215
216 import_backup_stream(context, backup_file, file_size, passphrase).await?;
217 Ok(())
218}
219
220pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
234 context: &Context,
235 backup_file: R,
236 file_size: u64,
237 passphrase: String,
238) -> Result<()> {
239 ensure!(
240 !context.is_configured().await?,
241 "Cannot import backups to accounts in use"
242 );
243 ensure!(
244 !context.scheduler.is_running().await,
245 "Cannot import backup, IO is running"
246 );
247
248 import_backup_stream_inner(context, backup_file, file_size, passphrase)
249 .await
250 .0
251}
252
253#[pin_project]
255struct ProgressReader<R> {
256 #[pin]
258 inner: R,
259
260 read: u64,
262
263 file_size: u64,
266
267 last_progress: u16,
269
270 context: Context,
272}
273
274impl<R> ProgressReader<R> {
275 fn new(r: R, context: Context, file_size: u64) -> Self {
276 Self {
277 inner: r,
278 read: 0,
279 file_size,
280 last_progress: 1,
281 context,
282 }
283 }
284}
285
286impl<R> AsyncRead for ProgressReader<R>
287where
288 R: AsyncRead,
289{
290 #[expect(clippy::arithmetic_side_effects)]
291 fn poll_read(
292 self: Pin<&mut Self>,
293 cx: &mut std::task::Context<'_>,
294 buf: &mut ReadBuf<'_>,
295 ) -> std::task::Poll<std::io::Result<()>> {
296 let this = self.project();
297 let before = buf.filled().len();
298 let res = this.inner.poll_read(cx, buf);
299 if let std::task::Poll::Ready(Ok(())) = res {
300 *this.read = this
301 .read
302 .saturating_add(usize_to_u64(buf.filled().len() - before));
303
304 let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999) as u16;
305 if progress > *this.last_progress {
306 this.context.emit_event(EventType::ImexProgress(progress));
307 *this.last_progress = progress;
308 }
309 }
310 res
311 }
312}
313
314async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
318 context: &Context,
319 backup_file: R,
320 file_size: u64,
321 passphrase: String,
322) -> (Result<()>,) {
323 let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
324 let mut archive = Archive::new(backup_file);
325
326 let mut entries = match archive.entries() {
327 Ok(entries) => entries,
328 Err(e) => return (Err(e).context("Failed to get archive entries"),),
329 };
330 let mut blobs = Vec::new();
331 let mut res: Result<()> = loop {
332 let mut f = match entries.try_next().await {
333 Ok(Some(f)) => f,
334 Ok(None) => break Ok(()),
335 Err(e) => break Err(e).context("Failed to get next entry"),
336 };
337
338 let path = match f.path() {
339 Ok(path) => path.to_path_buf(),
340 Err(e) => break Err(e).context("Failed to get entry path"),
341 };
342 if let Err(e) = f.unpack_in(context.get_blobdir()).await {
343 break Err(e).context("Failed to unpack file");
344 }
345 if path.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
346 continue;
347 }
348 let from_path = context.get_blobdir().join(&path);
350 if from_path.is_file() {
351 if let Some(name) = from_path.file_name() {
352 let to_path = context.get_blobdir().join(name);
353 if let Err(e) = fs::rename(&from_path, &to_path).await {
354 blobs.push(from_path);
355 break Err(e).context("Failed to move file to blobdir");
356 }
357 blobs.push(to_path);
358 } else {
359 warn!(context, "No file name");
360 }
361 }
362 };
363
364 let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
365 if res.is_ok() {
366 res = context
367 .sql
368 .import(&unpacked_database, passphrase.clone())
369 .await
370 .context("cannot import unpacked database");
371 }
372 if res.is_ok() {
373 res = check_backup_version(context).await;
374 }
375 fs::remove_file(unpacked_database)
376 .await
377 .context("cannot remove unpacked database")
378 .log_err(context)
379 .ok();
380 if res.is_ok() {
381 context.emit_event(EventType::ImexProgress(999));
382 res = context.sql.run_migrations(context).await;
383 context.emit_event(EventType::AccountsItemChanged);
384 }
385 if res.is_err() {
386 context.sql.close().await;
387 fs::remove_file(context.sql.dbfile.as_path())
388 .await
389 .log_err(context)
390 .ok();
391 for blob in blobs {
392 fs::remove_file(&blob).await.log_err(context).ok();
393 }
394 context
395 .sql
396 .open(context, "".to_string())
397 .await
398 .log_err(context)
399 .ok();
400 }
401 if res.is_ok() {
402 delete_and_reset_all_device_msgs(context)
403 .await
404 .log_err(context)
405 .ok();
406 }
407 (res,)
408}
409
410fn get_next_backup_path(
418 folder: &Path,
419 addr: &str,
420 backup_time: i64,
421) -> Result<(PathBuf, PathBuf, PathBuf)> {
422 let folder = PathBuf::from(folder);
423 let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
424 .context("can't get next backup path")?
425 .format("delta-chat-backup-%Y-%m-%d")
427 .to_string();
428
429 for i in 0..64 {
431 let mut tempdbfile = folder.clone();
432 tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
433
434 let mut tempfile = folder.clone();
435 tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
436
437 let mut destfile = folder.clone();
438 destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
439
440 if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
441 return Ok((tempdbfile, tempfile, destfile));
442 }
443 }
444 bail!("could not create backup file, disk full?");
445}
446
447#[expect(clippy::arithmetic_side_effects)]
451async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
452 let now = time();
454 let self_addr = context.get_primary_self_addr().await?;
455 let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
456 let temp_db_path = TempPathGuard::new(temp_db_path);
457 let temp_path = TempPathGuard::new(temp_path);
458
459 export_database(context, &temp_db_path, passphrase, now)
460 .await
461 .context("could not export database")?;
462
463 info!(
464 context,
465 "Backup '{}' to '{}'.",
466 context.get_dbfile().display(),
467 dest_path.display(),
468 );
469
470 let file = File::create(&temp_path).await?;
471 let blobdir = BlobDirContents::new(context).await?;
472
473 let mut file_size = 0;
474 file_size += temp_db_path.metadata()?.len();
475 for blob in blobdir.iter() {
476 file_size += blob.to_abs_path().metadata()?.len()
477 }
478
479 export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
480 .await
481 .context("Exporting backup to file failed")?;
482 fs::rename(temp_path, &dest_path).await?;
483 context.emit_event(EventType::ImexFileWritten(dest_path));
484 Ok(())
485}
486
487#[pin_project]
489struct ProgressWriter<W> {
490 #[pin]
492 inner: W,
493
494 written: u64,
496
497 file_size: u64,
500
501 last_progress: u16,
503
504 context: Context,
506}
507
508impl<W> ProgressWriter<W> {
509 fn new(w: W, context: Context, file_size: u64) -> Self {
510 Self {
511 inner: w,
512 written: 0,
513 file_size,
514 last_progress: 1,
515 context,
516 }
517 }
518}
519
520impl<W> AsyncWrite for ProgressWriter<W>
521where
522 W: AsyncWrite,
523{
524 #[expect(clippy::arithmetic_side_effects)]
525 fn poll_write(
526 self: Pin<&mut Self>,
527 cx: &mut std::task::Context<'_>,
528 buf: &[u8],
529 ) -> std::task::Poll<Result<usize, std::io::Error>> {
530 let this = self.project();
531 let res = this.inner.poll_write(cx, buf);
532 if let std::task::Poll::Ready(Ok(written)) = res {
533 *this.written = this.written.saturating_add(usize_to_u64(written));
534
535 let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999) as u16;
536 if progress > *this.last_progress {
537 this.context.emit_event(EventType::ImexProgress(progress));
538 *this.last_progress = progress;
539 }
540 }
541 res
542 }
543
544 fn poll_flush(
545 self: Pin<&mut Self>,
546 cx: &mut std::task::Context<'_>,
547 ) -> std::task::Poll<Result<(), std::io::Error>> {
548 self.project().inner.poll_flush(cx)
549 }
550
551 fn poll_shutdown(
552 self: Pin<&mut Self>,
553 cx: &mut std::task::Context<'_>,
554 ) -> std::task::Poll<Result<(), std::io::Error>> {
555 self.project().inner.poll_shutdown(cx)
556 }
557}
558
559pub(crate) async fn export_backup_stream<'a, W>(
561 context: &'a Context,
562 temp_db_path: &Path,
563 blobdir: BlobDirContents<'a>,
564 writer: W,
565 file_size: u64,
566) -> Result<()>
567where
568 W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
569{
570 let writer = ProgressWriter::new(writer, context.clone(), file_size);
571 let mut builder = tokio_tar::Builder::new(writer);
572
573 builder
574 .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
575 .await?;
576
577 for blob in blobdir.iter() {
578 let mut file = File::open(blob.to_abs_path()).await?;
579 let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
580 builder.append_file(path_in_archive, &mut file).await?;
581 }
582
583 builder.finish().await?;
584 Ok(())
585}
586
587async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
589 let buf = read_file(context, path).await?;
590 let armored = std::string::String::from_utf8_lossy(&buf);
591 set_self_key(context, &armored).await?;
592 Ok(())
593}
594
595#[expect(clippy::arithmetic_side_effects)]
605async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
606 let attr = tokio::fs::metadata(path).await?;
607
608 if attr.is_file() {
609 info!(
610 context,
611 "Importing secret key from {} as the default key.",
612 path.display()
613 );
614 import_secret_key(context, path).await?;
615 return Ok(());
616 }
617
618 let mut imported_cnt = 0;
619
620 let mut dir_handle = tokio::fs::read_dir(&path).await?;
621 while let Ok(Some(entry)) = dir_handle.next_entry().await {
622 let entry_fn = entry.file_name();
623 let name_f = entry_fn.to_string_lossy();
624 let path_plus_name = path.join(&entry_fn);
625 if let Some(suffix) = get_filesuffix_lc(&name_f) {
626 if suffix != "asc" {
627 continue;
628 }
629 } else {
630 continue;
631 };
632 info!(
633 context,
634 "Considering key file: {}.",
635 path_plus_name.display()
636 );
637
638 if let Err(err) = import_secret_key(context, &path_plus_name).await {
639 warn!(
640 context,
641 "Failed to import secret key from {}: {:#}.",
642 path_plus_name.display(),
643 err
644 );
645 continue;
646 }
647
648 imported_cnt += 1;
649 }
650 ensure!(
651 imported_cnt > 0,
652 "No private keys found in {}.",
653 path.display()
654 );
655 Ok(())
656}
657
658#[expect(clippy::arithmetic_side_effects)]
659async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
660 let mut export_errors = 0;
661
662 let keys = context
663 .sql
664 .query_map_vec(
665 "SELECT id, private_key, id=(SELECT value FROM config WHERE keyname='key_id') FROM keypairs;",
666 (),
667 |row| {
668 let id = row.get(0)?;
669 let private_key_blob: Vec<u8> = row.get(1)?;
670 let private_key = SignedSecretKey::from_slice(&private_key_blob);
671 let is_default: i32 = row.get(2)?;
672
673 Ok((id, private_key, is_default))
674 },
675 )
676 .await?;
677 let self_addr = context.get_primary_self_addr().await?;
678 for (id, private_key, is_default) in keys {
679 let id = Some(id).filter(|_| is_default == 0);
680
681 let Ok(private_key) = private_key else {
682 export_errors += 1;
683 continue;
684 };
685
686 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &private_key).await {
687 error!(context, "Failed to export private key: {:#}.", err);
688 export_errors += 1;
689 }
690
691 let public_key = private_key.to_public_key();
692
693 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &public_key).await {
694 error!(context, "Failed to export public key: {:#}.", err);
695 export_errors += 1;
696 }
697 }
698
699 ensure!(export_errors == 0, "errors while exporting keys");
700 Ok(())
701}
702
703async fn export_key_to_asc_file<T>(
705 context: &Context,
706 dir: &Path,
707 addr: &str,
708 id: Option<i64>,
709 key: &T,
710) -> Result<String>
711where
712 T: DcKey,
713{
714 let file_name = {
715 let kind = match T::is_private() {
716 false => "public",
717 true => "private",
718 };
719 let id = id.map_or("default".into(), |i| i.to_string());
720 let fp = key.dc_fingerprint().hex();
721 format!("{kind}-key-{addr}-{id}-{fp}.asc")
722 };
723 let path = dir.join(&file_name);
724 info!(context, "Exporting key to {}.", path.display());
725
726 delete_file(context, &path).await.ok();
728
729 let content = key.to_asc(None).into_bytes();
730 write_file(context, &path, &content)
731 .await
732 .with_context(|| format!("cannot write key to {}", path.display()))?;
733 context.emit_event(EventType::ImexFileWritten(path));
734 Ok(file_name)
735}
736
737async fn export_database(
744 context: &Context,
745 dest: &Path,
746 passphrase: String,
747 timestamp: i64,
748) -> Result<()> {
749 ensure!(
750 !context.scheduler.is_running().await,
751 "cannot export backup, IO is running"
752 );
753 let timestamp = timestamp.try_into().context("32-bit UNIX time overflow")?;
754
755 let dest = dest
757 .to_str()
758 .with_context(|| format!("path {} is not valid unicode", dest.display()))?;
759
760 context.set_config(Config::BccSelf, Some("1")).await?;
761 context
762 .sql
763 .set_raw_config_int("backup_time", timestamp)
764 .await?;
765 context
766 .sql
767 .set_raw_config_int("backup_version", DCBACKUP_VERSION)
768 .await?;
769 sql::housekeeping(context).await.log_err(context).ok();
770 context
771 .sql
772 .call_write(|conn| {
773 conn.execute("VACUUM;", ())
774 .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
775 .ok();
776 conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
777 .context("failed to attach backup database")?;
778 let res = conn
779 .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
780 .context("failed to export to attached backup database");
781 conn.execute(
782 "UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
783 [],
784 )
785 .ok(); conn.execute("DETACH DATABASE backup", [])
787 .context("failed to detach backup database")?;
788 res?;
789 Ok(())
790 })
791 .await
792}
793
794async fn check_backup_version(context: &Context) -> Result<()> {
795 let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
796 ensure!(
797 version <= DCBACKUP_VERSION,
798 "This profile is from a newer version of Delta Chat. Please update Delta Chat and try again (profile version is v{version}, the latest supported is v{DCBACKUP_VERSION})"
799 );
800 Ok(())
801}
802
803#[cfg(test)]
804mod tests {
805 use std::time::Duration;
806
807 use tokio::task;
808
809 use super::*;
810 use crate::config::Config;
811 use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
812
813 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
814 async fn test_export_public_key_to_asc_file() {
815 let context = TestContext::new().await;
816 let key = alice_keypair().to_public_key();
817 let blobdir = Path::new("$BLOBDIR");
818 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
819 .await
820 .unwrap();
821 assert!(filename.starts_with("public-key-a@b-default-"));
822 assert!(filename.ends_with(".asc"));
823 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
824 let filename = format!("{blobdir}/{filename}");
825 let bytes = tokio::fs::read(&filename).await.unwrap();
826
827 assert_eq!(bytes, key.to_asc(None).into_bytes());
828 }
829
830 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
831 async fn test_import_private_key_exported_to_asc_file() {
832 let context = TestContext::new().await;
833 let key = alice_keypair();
834 let blobdir = Path::new("$BLOBDIR");
835 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
836 .await
837 .unwrap();
838 let fingerprint = filename
839 .strip_prefix("private-key-a@b-default-")
840 .unwrap()
841 .strip_suffix(".asc")
842 .unwrap();
843 assert_eq!(fingerprint, key.dc_fingerprint().hex());
844 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
845 let filename = format!("{blobdir}/{filename}");
846 let bytes = tokio::fs::read(&filename).await.unwrap();
847
848 assert_eq!(bytes, key.to_asc(None).into_bytes());
849
850 let alice = &TestContext::new().await;
851 if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
852 panic!("got error on import: {err:#}");
853 }
854 }
855
856 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
857 async fn test_export_and_import_key_from_dir() {
858 let export_dir = tempfile::tempdir().unwrap();
859
860 let context = TestContext::new_alice().await;
861 if let Err(err) = imex(
862 &context.ctx,
863 ImexMode::ExportSelfKeys,
864 export_dir.path(),
865 None,
866 )
867 .await
868 {
869 panic!("got error on export: {err:#}");
870 }
871
872 let context2 = TestContext::new().await;
873 if let Err(err) = imex(
874 &context2.ctx,
875 ImexMode::ImportSelfKeys,
876 export_dir.path(),
877 None,
878 )
879 .await
880 {
881 panic!("got error on import: {err:#}");
882 }
883 }
884
885 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
886 async fn test_import_second_key() -> Result<()> {
887 let alice = &TestContext::new_alice().await;
888 let chat = alice.create_chat(alice).await;
889 let sent = alice.send_text(chat.id, "Encrypted with old key").await;
890 let export_dir = tempfile::tempdir().unwrap();
891
892 let alice = &TestContext::new().await;
893 alice.configure_addr("alice@example.org").await;
894 imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
895
896 let alice = &TestContext::new_alice().await;
897 let old_key = key::load_self_secret_key(alice).await?;
898
899 assert!(
900 imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
901 .await
902 .is_err()
903 );
904
905 assert_eq!(key::load_self_secret_key(alice).await?, old_key);
908
909 assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
910
911 let msg = alice.recv_msg(&sent).await;
912 assert!(msg.get_showpadlock());
913 assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
914 assert_eq!(msg.get_text(), "Encrypted with old key");
915
916 Ok(())
917 }
918
919 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
920 async fn test_export_and_import_backup() -> Result<()> {
921 let backup_dir = tempfile::tempdir().unwrap();
922
923 let context1 = TestContext::new_alice().await;
924 assert!(context1.is_configured().await?);
925
926 let context2 = TestContext::new().await;
927 assert!(!context2.is_configured().await?);
928 assert!(has_backup(&context2, backup_dir.path()).await.is_err());
929
930 assert!(
932 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
933 .await
934 .is_ok()
935 );
936 let _event = context1
937 .evtracker
938 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
939 .await;
940
941 let backup = has_backup(&context2, backup_dir.path()).await?;
943
944 assert!(
946 imex(
947 &context2,
948 ImexMode::ImportBackup,
949 backup.as_ref(),
950 Some("foobar".to_string())
951 )
952 .await
953 .is_err()
954 );
955
956 assert!(
957 imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
958 .await
959 .is_ok()
960 );
961 let _event = context2
962 .evtracker
963 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
964 .await;
965
966 assert!(context2.is_configured().await?);
967 assert_eq!(
968 context2.get_config(Config::Addr).await?,
969 Some("alice@example.org".to_string())
970 );
971 Ok(())
972 }
973
974 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
975 async fn test_export_import_chatmail_backup() -> Result<()> {
976 let backup_dir = tempfile::tempdir().unwrap();
977
978 let context1 = &TestContext::new_alice().await;
979
980 context1.set_config(Config::BccSelf, None).await?;
982
983 assert_eq!(
985 context1.get_config(Config::DeleteServerAfter).await?,
986 Some("0".to_string())
987 );
988 context1.set_config_bool(Config::IsChatmail, true).await?;
989 assert_eq!(
990 context1.get_config(Config::BccSelf).await?,
991 Some("0".to_string())
992 );
993 assert_eq!(
994 context1.get_config(Config::DeleteServerAfter).await?,
995 Some("1".to_string())
996 );
997
998 assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
999 imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1000 let _event = context1
1001 .evtracker
1002 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1003 .await;
1004
1005 let context2 = &TestContext::new().await;
1006 let backup = has_backup(context2, backup_dir.path()).await?;
1007 imex(context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
1008 let _event = context2
1009 .evtracker
1010 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1011 .await;
1012 assert!(context2.is_configured().await?);
1013 assert!(context2.is_chatmail().await?);
1014 for ctx in [context1, context2] {
1015 assert_eq!(
1016 ctx.get_config(Config::BccSelf).await?,
1017 Some("1".to_string())
1018 );
1019 assert_eq!(
1020 ctx.get_config(Config::DeleteServerAfter).await?,
1021 Some("0".to_string())
1022 );
1023 assert_eq!(ctx.get_config_delete_server_after().await?, None);
1024 }
1025 Ok(())
1026 }
1027
1028 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1030 async fn test_import_backup_fails_because_of_dcbackup_version() -> Result<()> {
1031 let mut tcm = TestContextManager::new();
1032 let context1 = tcm.alice().await;
1033 let context2 = tcm.unconfigured().await;
1034
1035 assert!(context1.is_configured().await?);
1036 assert!(!context2.is_configured().await?);
1037
1038 let backup_dir = tempfile::tempdir().unwrap();
1039
1040 tcm.section("export from context1");
1041 assert!(
1042 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
1043 .await
1044 .is_ok()
1045 );
1046 let _event = context1
1047 .evtracker
1048 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1049 .await;
1050 let backup = has_backup(&context2, backup_dir.path()).await?;
1051 let modified_backup = backup_dir.path().join("modified_backup.tar");
1052
1053 tcm.section("Change backup_version to be higher than DCBACKUP_VERSION");
1054 {
1055 let unpack_dir = tempfile::tempdir().unwrap();
1056 let mut ar = Archive::new(File::open(&backup).await?);
1057 ar.unpack(&unpack_dir).await?;
1058
1059 let sql = sql::Sql::new(unpack_dir.path().join(DBFILE_BACKUP_NAME));
1060 sql.open(&context2, "".to_string()).await?;
1061 assert_eq!(
1062 sql.get_raw_config_int("backup_version").await?.unwrap(),
1063 DCBACKUP_VERSION
1064 );
1065 sql.set_raw_config_int("backup_version", DCBACKUP_VERSION + 1)
1066 .await?;
1067 sql.close().await;
1068
1069 let modified_backup_file = File::create(&modified_backup).await?;
1070 let mut builder = tokio_tar::Builder::new(modified_backup_file);
1071 builder.append_dir_all("", unpack_dir.path()).await?;
1072 builder.finish().await?;
1073 }
1074
1075 tcm.section("import to context2");
1076 let err = imex(&context2, ImexMode::ImportBackup, &modified_backup, None)
1077 .await
1078 .unwrap_err();
1079 assert!(err.to_string().starts_with("This profile is from a newer version of Delta Chat. Please update Delta Chat and try again"));
1080
1081 let err_event = context2
1084 .evtracker
1085 .get_matching(|evt| matches!(evt, EventType::Error(_)))
1086 .await;
1087 let EventType::Error(err_msg) = err_event else {
1088 unreachable!()
1089 };
1090 assert!(err_msg.starts_with("This profile is from a newer version of Delta Chat. Please update Delta Chat and try again"));
1091
1092 context2
1093 .evtracker
1094 .get_matching(|evt| matches!(evt, EventType::ImexProgress(0)))
1095 .await;
1096
1097 assert!(!context2.is_configured().await?);
1098 assert_eq!(context2.get_config(Config::ConfiguredAddr).await?, None);
1099
1100 Ok(())
1101 }
1102
1103 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1107 async fn test_import_backup_reset_config_cache() -> Result<()> {
1108 let backup_dir = tempfile::tempdir()?;
1109 let context1 = TestContext::new_alice().await;
1110 let context2 = TestContext::new().await;
1111 assert!(!context2.is_configured().await?);
1112
1113 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1115
1116 let backup = has_backup(&context2, backup_dir.path()).await?;
1118 let context2_cloned = context2.clone();
1119 let handle = task::spawn(async move {
1120 imex(
1121 &context2_cloned,
1122 ImexMode::ImportBackup,
1123 backup.as_ref(),
1124 None,
1125 )
1126 .await
1127 .unwrap();
1128 });
1129
1130 while !handle.is_finished() {
1131 context2.is_configured().await.ok();
1134 tokio::time::sleep(Duration::from_micros(1)).await;
1135 }
1136
1137 assert!(context2.is_configured().await?);
1139
1140 Ok(())
1141 }
1142}