deltachat/
imex.rs

1//! # Import/export module.
2
3use 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
38// Name of the database file in the backup.
39const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
40pub(crate) const BLOBS_BACKUP_NAME: &str = "blobs_backup";
41
42/// Import/export command.
43#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
44#[repr(u32)]
45pub enum ImexMode {
46    /// Export all private keys and all public keys of the user to the
47    /// directory given as `path`. The default key is written to the files
48    /// `{public,private}-key-<addr>-default-<fingerprint>.asc`, if there are more keys, they are
49    /// written to files as `{public,private}-key-<addr>-<id>-<fingerprint>.asc`.
50    ExportSelfKeys = 1,
51
52    /// Import private keys found in `path` if it is a directory, otherwise import a private key
53    /// from `path`.
54    /// The last imported key is made the default keys unless its name contains the string `legacy`.
55    /// Public keys are not imported.
56    ImportSelfKeys = 2,
57
58    /// Export a backup to the directory given as `path` with the given `passphrase`.
59    /// The backup contains all contacts, chats, images and other data and device independent settings.
60    /// The backup does not contain device dependent settings as ringtones or LED notification settings.
61    /// The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
62    ExportBackup = 11,
63
64    /// `path` is the file (not: directory) to import. The file is normally
65    /// created by DC_IMEX_EXPORT_BACKUP and detected by imex_has_backup(). Importing a backup
66    /// is only possible as long as the context is not configured or used in another way.
67    ImportBackup = 12,
68}
69
70/// Import/export things.
71///
72/// What to do is defined by the `what` parameter.
73///
74/// During execution of the job,
75/// some events are sent out:
76///
77/// - A number of `DC_EVENT_IMEX_PROGRESS` events are sent and may be used to create
78///   a progress bar or stuff like that. Moreover, you'll be informed when the imex-job is done.
79///
80/// - For each file written on export, the function sends `DC_EVENT_IMEX_FILE_WRITTEN`
81///
82/// Only one import-/export-progress can run at the same time.
83/// To cancel an import-/export-progress, drop the future returned by this function.
84pub 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        // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
105        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
115/// Returns the filename of the backup found (otherwise an error)
116pub 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            // We just use string comparison to determine which backup is newer.
130            // This works fine because the filenames have the form `delta-chat-backup-2023-10-18-00-foo@example.com.tar`
131            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        // before we export anything, make sure the private key exists
180        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
200/// Imports backup into the currently open database.
201///
202/// The contents of the currently open database will be lost.
203///
204/// `passphrase` is the passphrase used to open backup database. If backup is unencrypted, pass
205/// empty string here.
206async 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
234/// Imports backup by reading a tar file from a stream.
235///
236/// `file_size` is used to calculate the progress
237/// and emit progress events.
238/// Ideally it is the sum of the entry
239/// sizes without the header overhead,
240/// but can be estimated as tar file size
241/// in which case the progress is underestimated
242/// and may not reach 99.9% by the end of import.
243/// Underestimating is better than
244/// overestimating because the progress
245/// jumps to 100% instead of getting stuck at 99.9%
246/// for some time.
247pub(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/// Reader that emits progress events as bytes are read from it.
259#[pin_project]
260struct ProgressReader<R> {
261    /// Wrapped reader.
262    #[pin]
263    inner: R,
264
265    /// Number of bytes successfully read from the internal reader.
266    read: usize,
267
268    /// Total size of the backup .tar file expected to be read from the reader.
269    /// Used to calculate the progress.
270    file_size: usize,
271
272    /// Last progress emitted to avoid emitting the same progress value twice.
273    last_progress: usize,
274
275    /// Context for emitting progress events.
276    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        // async_tar unpacked to $BLOBDIR/BLOBS_BACKUP_NAME/, so we move the file afterwards.
348        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
401/*******************************************************************************
402 * Export backup
403 ******************************************************************************/
404
405/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
406/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
407/// it can be renamed to dest_path. This guarantees that the backup is complete.
408fn 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        // Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
417        .format("delta-chat-backup-%Y-%m-%d")
418        .to_string();
419
420    // 64 backup files per day should be enough for everyone
421    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
438/// Exports the database to a separate file with the given passphrase.
439///
440/// Set passphrase to empty string to export the database unencrypted.
441async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
442    // get a fine backup file name (the name includes the date so that multiple backup instances are possible)
443    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/// Writer that emits progress events as bytes are written into it.
478#[pin_project]
479struct ProgressWriter<W> {
480    /// Wrapped writer.
481    #[pin]
482    inner: W,
483
484    /// Number of bytes successfully written into the internal writer.
485    written: usize,
486
487    /// Total size of the backup .tar file expected to be written into the writer.
488    /// Used to calculate the progress.
489    file_size: usize,
490
491    /// Last progress emitted to avoid emitting the same progress value twice.
492    last_progress: usize,
493
494    /// Context for emitting progress events.
495    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
548/// Exports the database and blobs into a stream.
549pub(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
576/// Imports secret key from a file.
577async 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
584/// Imports secret keys from the provided file or directory.
585///
586/// If provided path is a file, ASCII-armored secret key is read from the file
587/// and set as the default key.
588///
589/// If provided path is a directory, all files with .asc extension
590/// containing secret keys are imported and the last successfully
591/// imported which does not contain "legacy" in its filename
592/// is set as the default.
593async 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
696/// Returns the exported key file name inside `dir`.
697async 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 the file if it already exists.
725    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
735/// Exports the database to *dest*, encrypted using *passphrase*.
736///
737/// The directory of *dest* must already exist, if *dest* itself exists it will be
738/// overwritten.
739///
740/// This also verifies that IO is not running during the export.
741async 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    // TODO: Maybe introduce camino crate for UTF-8 paths where we need them.
754    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(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
784            conn.execute("DETACH DATABASE backup", [])
785                .context("failed to detach backup database")?;
786            res?;
787            Ok(())
788        })
789        .await
790}
791
792/// Sets `Config::BccSelf` (and `DeleteServerAfter` to "never" in effect) if needed so that new
793/// messages are present on the server after a backup restoration or available for all devices in
794/// multi-device case. NB: Calling this after a backup import isn't reliable as we can crash in
795/// between, but this is a problem only for old backups, new backups already have `BccSelf` set if
796/// necessary.
797async 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        // Importing a second key is not allowed anymore,
916        // even as a non-default key.
917        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            // export from context1
947            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            // import to context2
958            let backup = has_backup(&context2, backup_dir.path()).await?;
959
960            // Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
961            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        // Check that the settings are displayed correctly.
1010        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    /// This is a regression test for
1059    /// https://github.com/deltachat/deltachat-android/issues/2263
1060    /// where the config cache wasn't reset properly after a backup.
1061    #[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        // export from context1
1069        imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1070
1071        // import to context2
1072        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            // The database is still unconfigured;
1087            // fill the config cache with the old value.
1088            context2.is_configured().await.ok();
1089            tokio::time::sleep(Duration::from_micros(1)).await;
1090        }
1091
1092        // Assert that the config cache has the new value now.
1093        assert!(context2.is_configured().await?);
1094
1095        Ok(())
1096    }
1097}