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, 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        // All recent backups have `bcc_self` set to "1" before export.
381        //
382        // Setting `bcc_self` to "1" on export was introduced on 2024-12-17
383        // in commit 21664125d798021be75f47d5b0d5006d338b4531
384        //
385        // We additionally try to set `bcc_self` to "1" after import here
386        // for compatibility with older backups,
387        // but eventually this code can be removed.
388        res = context.set_config(Config::BccSelf, Some("1")).await;
389    }
390    fs::remove_file(unpacked_database)
391        .await
392        .context("cannot remove unpacked database")
393        .log_err(context)
394        .ok();
395    if res.is_ok() {
396        context.emit_event(EventType::ImexProgress(999));
397        res = context.sql.run_migrations(context).await;
398        context.emit_event(EventType::AccountsItemChanged);
399    }
400    if res.is_ok() {
401        delete_and_reset_all_device_msgs(context)
402            .await
403            .log_err(context)
404            .ok();
405    }
406    (res,)
407}
408
409/*******************************************************************************
410 * Export backup
411 ******************************************************************************/
412
413/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
414/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
415/// it can be renamed to dest_path. This guarantees that the backup is complete.
416fn get_next_backup_path(
417    folder: &Path,
418    addr: &str,
419    backup_time: i64,
420) -> Result<(PathBuf, PathBuf, PathBuf)> {
421    let folder = PathBuf::from(folder);
422    let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
423        .context("can't get next backup path")?
424        // Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
425        .format("delta-chat-backup-%Y-%m-%d")
426        .to_string();
427
428    // 64 backup files per day should be enough for everyone
429    for i in 0..64 {
430        let mut tempdbfile = folder.clone();
431        tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
432
433        let mut tempfile = folder.clone();
434        tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
435
436        let mut destfile = folder.clone();
437        destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
438
439        if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
440            return Ok((tempdbfile, tempfile, destfile));
441        }
442    }
443    bail!("could not create backup file, disk full?");
444}
445
446/// Exports the database to a separate file with the given passphrase.
447///
448/// Set passphrase to empty string to export the database unencrypted.
449async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
450    // get a fine backup file name (the name includes the date so that multiple backup instances are possible)
451    let now = time();
452    let self_addr = context.get_primary_self_addr().await?;
453    let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
454    let temp_db_path = TempPathGuard::new(temp_db_path);
455    let temp_path = TempPathGuard::new(temp_path);
456
457    export_database(context, &temp_db_path, passphrase, now)
458        .await
459        .context("could not export database")?;
460
461    info!(
462        context,
463        "Backup '{}' to '{}'.",
464        context.get_dbfile().display(),
465        dest_path.display(),
466    );
467
468    let file = File::create(&temp_path).await?;
469    let blobdir = BlobDirContents::new(context).await?;
470
471    let mut file_size = 0;
472    file_size += temp_db_path.metadata()?.len();
473    for blob in blobdir.iter() {
474        file_size += blob.to_abs_path().metadata()?.len()
475    }
476
477    export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
478        .await
479        .context("Exporting backup to file failed")?;
480    fs::rename(temp_path, &dest_path).await?;
481    context.emit_event(EventType::ImexFileWritten(dest_path));
482    Ok(())
483}
484
485/// Writer that emits progress events as bytes are written into it.
486#[pin_project]
487struct ProgressWriter<W> {
488    /// Wrapped writer.
489    #[pin]
490    inner: W,
491
492    /// Number of bytes successfully written into the internal writer.
493    written: usize,
494
495    /// Total size of the backup .tar file expected to be written into the writer.
496    /// Used to calculate the progress.
497    file_size: usize,
498
499    /// Last progress emitted to avoid emitting the same progress value twice.
500    last_progress: usize,
501
502    /// Context for emitting progress events.
503    context: Context,
504}
505
506impl<W> ProgressWriter<W> {
507    fn new(w: W, context: Context, file_size: u64) -> Self {
508        Self {
509            inner: w,
510            written: 0,
511            file_size: file_size as usize,
512            last_progress: 1,
513            context,
514        }
515    }
516}
517
518impl<W> AsyncWrite for ProgressWriter<W>
519where
520    W: AsyncWrite,
521{
522    fn poll_write(
523        self: Pin<&mut Self>,
524        cx: &mut std::task::Context<'_>,
525        buf: &[u8],
526    ) -> std::task::Poll<Result<usize, std::io::Error>> {
527        let this = self.project();
528        let res = this.inner.poll_write(cx, buf);
529        if let std::task::Poll::Ready(Ok(written)) = res {
530            *this.written = this.written.saturating_add(written);
531
532            let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999);
533            if progress > *this.last_progress {
534                this.context.emit_event(EventType::ImexProgress(progress));
535                *this.last_progress = progress;
536            }
537        }
538        res
539    }
540
541    fn poll_flush(
542        self: Pin<&mut Self>,
543        cx: &mut std::task::Context<'_>,
544    ) -> std::task::Poll<Result<(), std::io::Error>> {
545        self.project().inner.poll_flush(cx)
546    }
547
548    fn poll_shutdown(
549        self: Pin<&mut Self>,
550        cx: &mut std::task::Context<'_>,
551    ) -> std::task::Poll<Result<(), std::io::Error>> {
552        self.project().inner.poll_shutdown(cx)
553    }
554}
555
556/// Exports the database and blobs into a stream.
557pub(crate) async fn export_backup_stream<'a, W>(
558    context: &'a Context,
559    temp_db_path: &Path,
560    blobdir: BlobDirContents<'a>,
561    writer: W,
562    file_size: u64,
563) -> Result<()>
564where
565    W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
566{
567    let writer = ProgressWriter::new(writer, context.clone(), file_size);
568    let mut builder = tokio_tar::Builder::new(writer);
569
570    builder
571        .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
572        .await?;
573
574    for blob in blobdir.iter() {
575        let mut file = File::open(blob.to_abs_path()).await?;
576        let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
577        builder.append_file(path_in_archive, &mut file).await?;
578    }
579
580    builder.finish().await?;
581    Ok(())
582}
583
584/// Imports secret key from a file.
585async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
586    let buf = read_file(context, path).await?;
587    let armored = std::string::String::from_utf8_lossy(&buf);
588    set_self_key(context, &armored).await?;
589    Ok(())
590}
591
592/// Imports secret keys from the provided file or directory.
593///
594/// If provided path is a file, ASCII-armored secret key is read from the file
595/// and set as the default key.
596///
597/// If provided path is a directory, all files with .asc extension
598/// containing secret keys are imported and the last successfully
599/// imported which does not contain "legacy" in its filename
600/// is set as the default.
601async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
602    let attr = tokio::fs::metadata(path).await?;
603
604    if attr.is_file() {
605        info!(
606            context,
607            "Importing secret key from {} as the default key.",
608            path.display()
609        );
610        import_secret_key(context, path).await?;
611        return Ok(());
612    }
613
614    let mut imported_cnt = 0;
615
616    let mut dir_handle = tokio::fs::read_dir(&path).await?;
617    while let Ok(Some(entry)) = dir_handle.next_entry().await {
618        let entry_fn = entry.file_name();
619        let name_f = entry_fn.to_string_lossy();
620        let path_plus_name = path.join(&entry_fn);
621        if let Some(suffix) = get_filesuffix_lc(&name_f) {
622            if suffix != "asc" {
623                continue;
624            }
625        } else {
626            continue;
627        };
628        info!(
629            context,
630            "Considering key file: {}.",
631            path_plus_name.display()
632        );
633
634        if let Err(err) = import_secret_key(context, &path_plus_name).await {
635            warn!(
636                context,
637                "Failed to import secret key from {}: {:#}.",
638                path_plus_name.display(),
639                err
640            );
641            continue;
642        }
643
644        imported_cnt += 1;
645    }
646    ensure!(
647        imported_cnt > 0,
648        "No private keys found in {}.",
649        path.display()
650    );
651    Ok(())
652}
653
654async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
655    let mut export_errors = 0;
656
657    let keys = context
658        .sql
659        .query_map_vec(
660            "SELECT id, public_key, private_key, id=(SELECT value FROM config WHERE keyname='key_id') FROM keypairs;",
661            (),
662            |row| {
663                let id = row.get(0)?;
664                let public_key_blob: Vec<u8> = row.get(1)?;
665                let public_key = SignedPublicKey::from_slice(&public_key_blob);
666                let private_key_blob: Vec<u8> = row.get(2)?;
667                let private_key = SignedSecretKey::from_slice(&private_key_blob);
668                let is_default: i32 = row.get(3)?;
669
670                Ok((id, public_key, private_key, is_default))
671            },
672        )
673        .await?;
674    let self_addr = context.get_primary_self_addr().await?;
675    for (id, public_key, private_key, is_default) in keys {
676        let id = Some(id).filter(|_| is_default == 0);
677
678        if let Ok(key) = public_key {
679            if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
680                error!(context, "Failed to export public key: {:#}.", err);
681                export_errors += 1;
682            }
683        } else {
684            export_errors += 1;
685        }
686        if let Ok(key) = private_key {
687            if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
688                error!(context, "Failed to export private key: {:#}.", err);
689                export_errors += 1;
690            }
691        } else {
692            export_errors += 1;
693        }
694    }
695
696    ensure!(export_errors == 0, "errors while exporting keys");
697    Ok(())
698}
699
700/// Returns the exported key file name inside `dir`.
701async fn export_key_to_asc_file<T>(
702    context: &Context,
703    dir: &Path,
704    addr: &str,
705    id: Option<i64>,
706    key: &T,
707) -> Result<String>
708where
709    T: DcKey,
710{
711    let file_name = {
712        let kind = match T::is_private() {
713            false => "public",
714            true => "private",
715        };
716        let id = id.map_or("default".into(), |i| i.to_string());
717        let fp = key.dc_fingerprint().hex();
718        format!("{kind}-key-{addr}-{id}-{fp}.asc")
719    };
720    let path = dir.join(&file_name);
721    info!(
722        context,
723        "Exporting key {:?} to {}.",
724        key.key_id(),
725        path.display()
726    );
727
728    // Delete the file if it already exists.
729    delete_file(context, &path).await.ok();
730
731    let content = key.to_asc(None).into_bytes();
732    write_file(context, &path, &content)
733        .await
734        .with_context(|| format!("cannot write key to {}", path.display()))?;
735    context.emit_event(EventType::ImexFileWritten(path));
736    Ok(file_name)
737}
738
739/// Exports the database to *dest*, encrypted using *passphrase*.
740///
741/// The directory of *dest* must already exist, if *dest* itself exists it will be
742/// overwritten.
743///
744/// This also verifies that IO is not running during the export.
745async fn export_database(
746    context: &Context,
747    dest: &Path,
748    passphrase: String,
749    timestamp: i64,
750) -> Result<()> {
751    ensure!(
752        !context.scheduler.is_running().await,
753        "cannot export backup, IO is running"
754    );
755    let timestamp = timestamp.try_into().context("32-bit UNIX time overflow")?;
756
757    // TODO: Maybe introduce camino crate for UTF-8 paths where we need them.
758    let dest = dest
759        .to_str()
760        .with_context(|| format!("path {} is not valid unicode", dest.display()))?;
761
762    context.set_config(Config::BccSelf, Some("1")).await?;
763    context
764        .sql
765        .set_raw_config_int("backup_time", timestamp)
766        .await?;
767    context
768        .sql
769        .set_raw_config_int("backup_version", DCBACKUP_VERSION)
770        .await?;
771    sql::housekeeping(context).await.log_err(context).ok();
772    context
773        .sql
774        .call_write(|conn| {
775            conn.execute("VACUUM;", ())
776                .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
777                .ok();
778            conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
779                .context("failed to attach backup database")?;
780            let res = conn
781                .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
782                .context("failed to export to attached backup database");
783            conn.execute(
784                "UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
785                [],
786            )
787            .ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
788            conn.execute("DETACH DATABASE backup", [])
789                .context("failed to detach backup database")?;
790            res?;
791            Ok(())
792        })
793        .await
794}
795
796async fn check_backup_version(context: &Context) -> Result<()> {
797    let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
798    ensure!(
799        version <= DCBACKUP_VERSION,
800        "Backup too new, please update Delta Chat"
801    );
802    Ok(())
803}
804
805#[cfg(test)]
806mod tests {
807    use std::time::Duration;
808
809    use tokio::task;
810
811    use super::*;
812    use crate::config::Config;
813    use crate::test_utils::{TestContext, alice_keypair};
814
815    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
816    async fn test_export_public_key_to_asc_file() {
817        let context = TestContext::new().await;
818        let key = alice_keypair().public;
819        let blobdir = Path::new("$BLOBDIR");
820        let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
821            .await
822            .unwrap();
823        assert!(filename.starts_with("public-key-a@b-default-"));
824        assert!(filename.ends_with(".asc"));
825        let blobdir = context.ctx.get_blobdir().to_str().unwrap();
826        let filename = format!("{blobdir}/{filename}");
827        let bytes = tokio::fs::read(&filename).await.unwrap();
828
829        assert_eq!(bytes, key.to_asc(None).into_bytes());
830    }
831
832    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
833    async fn test_import_private_key_exported_to_asc_file() {
834        let context = TestContext::new().await;
835        let key = alice_keypair().secret;
836        let blobdir = Path::new("$BLOBDIR");
837        let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
838            .await
839            .unwrap();
840        let fingerprint = filename
841            .strip_prefix("private-key-a@b-default-")
842            .unwrap()
843            .strip_suffix(".asc")
844            .unwrap();
845        assert_eq!(fingerprint, key.dc_fingerprint().hex());
846        let blobdir = context.ctx.get_blobdir().to_str().unwrap();
847        let filename = format!("{blobdir}/{filename}");
848        let bytes = tokio::fs::read(&filename).await.unwrap();
849
850        assert_eq!(bytes, key.to_asc(None).into_bytes());
851
852        let alice = &TestContext::new().await;
853        if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
854            panic!("got error on import: {err:#}");
855        }
856    }
857
858    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
859    async fn test_export_and_import_key_from_dir() {
860        let export_dir = tempfile::tempdir().unwrap();
861
862        let context = TestContext::new_alice().await;
863        if let Err(err) = imex(
864            &context.ctx,
865            ImexMode::ExportSelfKeys,
866            export_dir.path(),
867            None,
868        )
869        .await
870        {
871            panic!("got error on export: {err:#}");
872        }
873
874        let context2 = TestContext::new().await;
875        if let Err(err) = imex(
876            &context2.ctx,
877            ImexMode::ImportSelfKeys,
878            export_dir.path(),
879            None,
880        )
881        .await
882        {
883            panic!("got error on import: {err:#}");
884        }
885    }
886
887    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
888    async fn test_import_second_key() -> Result<()> {
889        let alice = &TestContext::new_alice().await;
890        let chat = alice.create_chat(alice).await;
891        let sent = alice.send_text(chat.id, "Encrypted with old key").await;
892        let export_dir = tempfile::tempdir().unwrap();
893
894        let alice = &TestContext::new().await;
895        alice.configure_addr("alice@example.org").await;
896        imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
897
898        let alice = &TestContext::new_alice().await;
899        let old_key = key::load_self_secret_key(alice).await?;
900
901        assert!(
902            imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
903                .await
904                .is_err()
905        );
906
907        // Importing a second key is not allowed anymore,
908        // even as a non-default key.
909        assert_eq!(key::load_self_secret_key(alice).await?, old_key);
910
911        assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
912
913        let msg = alice.recv_msg(&sent).await;
914        assert!(msg.get_showpadlock());
915        assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
916        assert_eq!(msg.get_text(), "Encrypted with old key");
917
918        Ok(())
919    }
920
921    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
922    async fn test_export_and_import_backup() -> Result<()> {
923        let backup_dir = tempfile::tempdir().unwrap();
924
925        let context1 = TestContext::new_alice().await;
926        assert!(context1.is_configured().await?);
927
928        let context2 = TestContext::new().await;
929        assert!(!context2.is_configured().await?);
930        assert!(has_backup(&context2, backup_dir.path()).await.is_err());
931
932        // export from context1
933        assert!(
934            imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
935                .await
936                .is_ok()
937        );
938        let _event = context1
939            .evtracker
940            .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
941            .await;
942
943        // import to context2
944        let backup = has_backup(&context2, backup_dir.path()).await?;
945
946        // Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
947        assert!(
948            imex(
949                &context2,
950                ImexMode::ImportBackup,
951                backup.as_ref(),
952                Some("foobar".to_string())
953            )
954            .await
955            .is_err()
956        );
957
958        assert!(
959            imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
960                .await
961                .is_ok()
962        );
963        let _event = context2
964            .evtracker
965            .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
966            .await;
967
968        assert!(context2.is_configured().await?);
969        assert_eq!(
970            context2.get_config(Config::Addr).await?,
971            Some("alice@example.org".to_string())
972        );
973        Ok(())
974    }
975
976    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
977    async fn test_export_import_chatmail_backup() -> Result<()> {
978        let backup_dir = tempfile::tempdir().unwrap();
979
980        let context1 = &TestContext::new_alice().await;
981
982        // `bcc_self` is enabled by default for test contexts. Unset it.
983        context1.set_config(Config::BccSelf, None).await?;
984
985        // Check that the settings are displayed correctly.
986        assert_eq!(
987            context1.get_config(Config::DeleteServerAfter).await?,
988            Some("0".to_string())
989        );
990        context1.set_config_bool(Config::IsChatmail, true).await?;
991        assert_eq!(
992            context1.get_config(Config::BccSelf).await?,
993            Some("0".to_string())
994        );
995        assert_eq!(
996            context1.get_config(Config::DeleteServerAfter).await?,
997            Some("1".to_string())
998        );
999
1000        assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
1001        imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1002        let _event = context1
1003            .evtracker
1004            .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1005            .await;
1006
1007        let context2 = &TestContext::new().await;
1008        let backup = has_backup(context2, backup_dir.path()).await?;
1009        imex(context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
1010        let _event = context2
1011            .evtracker
1012            .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1013            .await;
1014        assert!(context2.is_configured().await?);
1015        assert!(context2.is_chatmail().await?);
1016        for ctx in [context1, context2] {
1017            assert_eq!(
1018                ctx.get_config(Config::BccSelf).await?,
1019                Some("1".to_string())
1020            );
1021            assert_eq!(
1022                ctx.get_config(Config::DeleteServerAfter).await?,
1023                Some("0".to_string())
1024            );
1025            assert_eq!(ctx.get_config_delete_server_after().await?, None);
1026        }
1027        Ok(())
1028    }
1029
1030    /// This is a regression test for
1031    /// https://github.com/deltachat/deltachat-android/issues/2263
1032    /// where the config cache wasn't reset properly after a backup.
1033    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1034    async fn test_import_backup_reset_config_cache() -> Result<()> {
1035        let backup_dir = tempfile::tempdir()?;
1036        let context1 = TestContext::new_alice().await;
1037        let context2 = TestContext::new().await;
1038        assert!(!context2.is_configured().await?);
1039
1040        // export from context1
1041        imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1042
1043        // import to context2
1044        let backup = has_backup(&context2, backup_dir.path()).await?;
1045        let context2_cloned = context2.clone();
1046        let handle = task::spawn(async move {
1047            imex(
1048                &context2_cloned,
1049                ImexMode::ImportBackup,
1050                backup.as_ref(),
1051                None,
1052            )
1053            .await
1054            .unwrap();
1055        });
1056
1057        while !handle.is_finished() {
1058            // The database is still unconfigured;
1059            // fill the config cache with the old value.
1060            context2.is_configured().await.ok();
1061            tokio::time::sleep(Duration::from_micros(1)).await;
1062        }
1063
1064        // Assert that the config cache has the new value now.
1065        assert!(context2.is_configured().await?);
1066
1067        Ok(())
1068    }
1069}