Skip to main content

deltachat/imex/
transfer.rs

1//! Transfer a backup to an other device.
2//!
3//! This module provides support for using [iroh](https://iroh.computer/)
4//! to initiate transfer of a backup to another device using a QR code.
5//!
6//! There are two parties to this:
7//! - The *Provider*, which starts a server and listens for connections.
8//! - The *Getter*, which connects to the server and retrieves the data.
9//!
10//! Both the provider and the getter are authenticated:
11//!
12//! - The provider is known by its *peer ID*.
13//! - The provider needs an *authentication token* from the getter before it accepts a
14//!   connection.
15//!
16//! Both these are transferred in the QR code offered to the getter.  This ensures that the
17//! getter can not connect to an impersonated provider and the provider does not offer the
18//! download to an impersonated getter.
19//!
20//! Protocol starts by getter opening a bidirectional QUIC stream
21//! to the provider and sending authentication token.
22//! Provider verifies received authentication token,
23//! sends the size of all files in a backup (database and all blobs)
24//! as an unsigned 64-bit big endian integer and streams the backup in tar format.
25//! Getter receives the backup and acknowledges successful reception
26//! by sending a single byte.
27//! Provider closes the endpoint after receiving an acknowledgment.
28
29use std::future::Future;
30use std::pin::Pin;
31use std::sync::Arc;
32use std::task::Poll;
33use std::time::Duration;
34
35use anyhow::{Context as _, Result, bail, format_err};
36use futures_lite::FutureExt;
37use iroh::{Endpoint, RelayMode};
38use tokio::fs;
39use tokio::task::JoinHandle;
40use tokio_util::sync::CancellationToken;
41
42use crate::EventType;
43use crate::chat::add_device_msg;
44use crate::context::Context;
45use crate::imex::BlobDirContents;
46use crate::key;
47use crate::log::warn;
48use crate::message::Message;
49use crate::qr::Qr;
50use crate::stock_str::backup_transfer_msg_body;
51use crate::tools::{TempPathGuard, create_id, time};
52
53use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
54
55/// ALPN protocol identifier for the backup transfer protocol.
56const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
57
58/// Provide or send a backup of this device.
59///
60/// This creates a backup of the current device and starts a service which offers another
61/// device to download this backup.
62///
63/// This does not make a full backup on disk, only the SQLite database is created on disk,
64/// the blobs in the blob directory are not copied.
65///
66/// This starts a task which acquires the global "ongoing" mutex.  If you need to stop the
67/// task use the [`Context::stop_ongoing`] mechanism.
68#[derive(Debug)]
69pub struct BackupProvider {
70    /// iroh endpoint.
71    _endpoint: Endpoint,
72
73    /// iroh address.
74    node_addr: iroh::NodeAddr,
75
76    /// Authentication token that should be submitted
77    /// to retrieve the backup.
78    auth_token: String,
79
80    /// Handle for the task accepting backup transfer requests.
81    handle: JoinHandle<Result<()>>,
82
83    /// Guard to cancel the provider on drop.
84    _drop_guard: tokio_util::sync::DropGuard,
85}
86
87impl BackupProvider {
88    /// Prepares for sending a backup to a second device.
89    ///
90    /// Before calling this function all I/O must be stopped so that no changes to the blobs
91    /// or database are happening, this is done by calling the [`Accounts::stop_io`] or
92    /// [`Context::stop_io`] APIs first.
93    ///
94    /// This will acquire the global "ongoing process" mutex, which can be used to cancel
95    /// the process.
96    ///
97    /// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
98    pub async fn prepare(context: &Context) -> Result<Self> {
99        let relay_mode = RelayMode::Disabled;
100        let endpoint = Endpoint::builder()
101            .tls_x509() // For compatibility with iroh <0.34.0
102            .alpns(vec![BACKUP_ALPN.to_vec()])
103            .relay_mode(relay_mode)
104            .bind()
105            .await?;
106        let node_addr = endpoint.node_addr().await?;
107
108        // Acquire global "ongoing" mutex.
109        let cancel_token = context.alloc_ongoing().await?;
110        let paused_guard = context.scheduler.pause(context).await?;
111        let context_dir = context
112            .get_blobdir()
113            .parent()
114            .context("Context dir not found")?;
115
116        // before we export, make sure the private key exists
117        key::ensure_secret_key_exists(context)
118            .await
119            .context("Cannot create private key or private key not available")?;
120
121        let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
122        if fs::metadata(&dbfile).await.is_ok() {
123            fs::remove_file(&dbfile).await?;
124            warn!(context, "Previous database export deleted");
125        }
126        let dbfile = TempPathGuard::new(dbfile);
127
128        // Authentication token that receiver should send us to receive a backup.
129        let auth_token = create_id();
130
131        let passphrase = String::new();
132
133        export_database(context, &dbfile, passphrase, time())
134            .await
135            .context("Database export failed")?;
136
137        let drop_token = CancellationToken::new();
138        let handle = {
139            let context = context.clone();
140            let drop_token = drop_token.clone();
141            let endpoint = endpoint.clone();
142            let auth_token = auth_token.clone();
143            tokio::spawn(async move {
144                Self::accept_loop(
145                    context.clone(),
146                    endpoint,
147                    auth_token,
148                    cancel_token,
149                    drop_token,
150                    dbfile,
151                )
152                .await;
153                info!(context, "Finished accept loop.");
154
155                context.free_ongoing().await;
156
157                // Explicit drop to move the guards into this future
158                drop(paused_guard);
159                Ok(())
160            })
161        };
162        Ok(Self {
163            _endpoint: endpoint,
164            node_addr,
165            auth_token,
166            handle,
167            _drop_guard: drop_token.drop_guard(),
168        })
169    }
170
171    async fn handle_connection(
172        context: Context,
173        conn: iroh::endpoint::Connecting,
174        auth_token: String,
175        dbfile: Arc<TempPathGuard>,
176    ) -> Result<()> {
177        let conn = conn.await?;
178        let (mut send_stream, mut recv_stream) = conn.accept_bi().await?;
179
180        // Read authentication token from the stream.
181        let mut received_auth_token = vec![0u8; auth_token.len()];
182        recv_stream.read_exact(&mut received_auth_token).await?;
183        if received_auth_token.as_slice() != auth_token.as_bytes() {
184            warn!(context, "Received wrong backup authentication token.");
185            return Ok(());
186        }
187
188        info!(context, "Received valid backup authentication token.");
189        // Emit a nonzero progress so that UIs can display smth like "Transferring...".
190        context.emit_event(EventType::ImexProgress(1));
191
192        let blobdir = BlobDirContents::new(&context).await?;
193
194        let mut file_size = dbfile.metadata()?.len();
195        for blob in blobdir.iter() {
196            file_size = file_size
197                .checked_add(blob.to_abs_path().metadata()?.len())
198                .context("File size overflow")?;
199        }
200
201        send_stream.write_all(&file_size.to_be_bytes()).await?;
202
203        export_backup_stream(&context, &dbfile, blobdir, send_stream, file_size)
204            .await
205            .context("Failed to write backup into QUIC stream")?;
206        info!(context, "Finished writing backup into QUIC stream.");
207        let mut buf = [0u8; 1];
208        info!(context, "Waiting for acknowledgment.");
209        recv_stream.read_exact(&mut buf).await?;
210        info!(context, "Received backup reception acknowledgement.");
211        context.emit_event(EventType::ImexProgress(1000));
212
213        let mut msg = Message::new_text(backup_transfer_msg_body(&context));
214        add_device_msg(&context, None, Some(&mut msg)).await?;
215
216        Ok(())
217    }
218
219    async fn accept_loop(
220        context: Context,
221        endpoint: Endpoint,
222        auth_token: String,
223        cancel_token: async_channel::Receiver<()>,
224        drop_token: CancellationToken,
225        dbfile: TempPathGuard,
226    ) {
227        let dbfile = Arc::new(dbfile);
228        loop {
229            tokio::select! {
230                biased;
231
232                conn = endpoint.accept() => {
233                    if let Some(conn) = conn {
234                        let conn = match conn.accept() {
235                            Ok(conn) => conn,
236                            Err(err) => {
237                               warn!(context, "Failed to accept iroh connection: {err:#}.");
238                               continue;
239                            }
240                        };
241                        // Got a new in-progress connection.
242                        let context = context.clone();
243                        let auth_token = auth_token.clone();
244                        let dbfile = dbfile.clone();
245                        if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
246                            async {
247                                cancel_token.recv().await.ok();
248                                Err(format_err!("Backup transfer canceled"))
249                            }
250                        ).race(
251                            async {
252                                drop_token.cancelled().await;
253                                Err(format_err!("Backup provider dropped"))
254                            }
255                        ).await {
256                            error!(context, "Error while handling backup connection: {err:#}.");
257                            context.emit_event(EventType::ImexProgress(0));
258                            break;
259                        } else {
260                            info!(context, "Backup transfer finished successfully.");
261                            break;
262                        }
263                    } else {
264                        break;
265                    }
266                },
267                _ = cancel_token.recv() => {
268                    info!(context, "Backup transfer canceled by the user, stopping accept loop.");
269                    context.emit_event(EventType::ImexProgress(0));
270                    break;
271                }
272                _ = drop_token.cancelled() => {
273                    info!(context, "Backup transfer canceled by dropping the provider, stopping accept loop.");
274                    context.emit_event(EventType::ImexProgress(0));
275                    break;
276                }
277            }
278        }
279    }
280
281    /// Returns a QR code that allows fetching this backup.
282    ///
283    /// This QR code can be passed to [`get_backup`] on a (different) device.
284    pub fn qr(&self) -> Qr {
285        Qr::Backup2 {
286            node_addr: self.node_addr.clone(),
287
288            auth_token: self.auth_token.clone(),
289        }
290    }
291}
292
293impl Future for BackupProvider {
294    type Output = Result<()>;
295
296    /// Waits for the backup transfer to complete.
297    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
298        Pin::new(&mut self.handle).poll(cx)?
299    }
300}
301
302pub async fn get_backup2(
303    context: &Context,
304    node_addr: iroh::NodeAddr,
305    auth_token: String,
306) -> Result<()> {
307    let relay_mode = RelayMode::Disabled;
308
309    let mut transport_config = iroh::endpoint::TransportConfig::default();
310    transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into()?));
311    let endpoint = Endpoint::builder()
312        .tls_x509() // For compatibility with iroh <0.34.0
313        .relay_mode(relay_mode)
314        .transport_config(transport_config)
315        .bind()
316        .await?;
317
318    let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
319    let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
320    info!(context, "Sending backup authentication token.");
321    send_stream.write_all(auth_token.as_bytes()).await?;
322
323    let passphrase = String::new();
324    info!(context, "Starting to read backup from the stream.");
325
326    let mut file_size_buf = [0u8; 8];
327    recv_stream.read_exact(&mut file_size_buf).await?;
328    let file_size = u64::from_be_bytes(file_size_buf);
329    info!(context, "Received backup file size.");
330    // Emit a nonzero progress so that UIs can display smth like "Transferring...".
331    context.emit_event(EventType::ImexProgress(1));
332
333    import_backup_stream(context, recv_stream, file_size, passphrase)
334        .await
335        .context("Failed to import backup from QUIC stream")?;
336    info!(context, "Finished importing backup from the stream.");
337    context.emit_event(EventType::ImexProgress(1000));
338
339    // Send an acknowledgement, but ignore the errors.
340    // We have imported backup successfully already.
341    send_stream.write_all(b".").await.ok();
342    send_stream.finish().ok();
343    info!(context, "Sent backup reception acknowledgment.");
344
345    // Wait for the peer to acknowledge reception of the acknowledgement
346    // before closing the connection.
347    _ = send_stream.stopped().await;
348
349    Ok(())
350}
351
352/// Contacts a backup provider and receives the backup from it.
353///
354/// This uses a QR code to contact another instance of deltachat which is providing a backup
355/// using the [`BackupProvider`].  Once connected it will authenticate using the secrets in
356/// the QR code and retrieve the backup.
357///
358/// This is a long running operation which will return only when completed.
359///
360/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it.  It
361/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
362/// having to revert to untyped bytes.
363pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
364    match qr {
365        Qr::Backup2 {
366            node_addr,
367            auth_token,
368        } => {
369            let cancel_token = context.alloc_ongoing().await?;
370            let res = get_backup2(context, node_addr, auth_token)
371                .race(async {
372                    cancel_token.recv().await.ok();
373                    Err(format_err!("Backup reception canceled"))
374                })
375                .await;
376            if let Err(ref res) = res {
377                error!(context, "{:#}", res);
378                context.emit_event(EventType::ImexProgress(0));
379            }
380            context.free_ongoing().await;
381            res?;
382        }
383        _ => bail!("QR code for backup must be of type DCBACKUP2"),
384    }
385    Ok(())
386}
387
388#[cfg(test)]
389mod tests {
390    use std::time::Duration;
391
392    use crate::chat::{ChatItem, get_chat_msgs, send_msg};
393    use crate::message::Viewtype;
394    use crate::test_utils::TestContextManager;
395
396    use super::*;
397
398    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
399    async fn test_send_receive() {
400        let mut tcm = TestContextManager::new();
401
402        // Create first device.
403        let ctx0 = tcm.alice().await;
404
405        // Write a message in the self chat
406        let self_chat = ctx0.get_self_chat().await;
407        let mut msg = Message::new_text("hi there".to_string());
408        send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
409
410        // Send an attachment in the self chat
411        let file = ctx0.get_blobdir().join("hello.txt");
412        fs::write(&file, "i am attachment").await.unwrap();
413        let mut msg = Message::new(Viewtype::File);
414        msg.set_file_and_deduplicate(&ctx0, &file, Some("hello.txt"), Some("text/plain"))
415            .unwrap();
416        send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
417
418        // Prepare to transfer backup.
419        let provider = BackupProvider::prepare(&ctx0).await.unwrap();
420
421        // Set up second device.
422        let ctx1 = tcm.unconfigured().await;
423        get_backup(&ctx1, provider.qr()).await.unwrap();
424
425        // Make sure the provider finishes without an error.
426        tokio::time::timeout(Duration::from_secs(30), provider)
427            .await
428            .expect("timed out")
429            .expect("error in provider");
430
431        // Check that we have the self message.
432        let self_chat = ctx1.get_self_chat().await;
433        let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap();
434        assert_eq!(msgs.len(), 2);
435        let ChatItem::Message { msg_id } = msgs.first().unwrap() else {
436            panic!("wrong chat item");
437        };
438        let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
439        let text = msg.get_text();
440        assert_eq!(text, "hi there");
441        let ChatItem::Message { msg_id } = msgs.get(1).unwrap() else {
442            panic!("wrong chat item");
443        };
444        let msg = Message::load_from_db(&ctx1, *msg_id).await.unwrap();
445
446        let path = msg.get_file(&ctx1).unwrap();
447        assert_eq!(
448            // That's the hash of the file:
449            path.with_file_name("ac1d2d284757656a8d41dc40aae4136.txt"),
450            path
451        );
452        assert_eq!("hello.txt", msg.get_filename().unwrap());
453        let text = fs::read_to_string(&path).await.unwrap();
454        assert_eq!(text, "i am attachment");
455
456        let path = path.with_file_name("saved.txt");
457        msg.save_file(&ctx1, &path).await.unwrap();
458        let text = fs::read_to_string(&path).await.unwrap();
459        assert_eq!(text, "i am attachment");
460        assert!(msg.save_file(&ctx1, &path).await.is_err());
461
462        // Check that both received the ImexProgress events.
463        for ctx in [&ctx0, &ctx1] {
464            ctx.evtracker
465                .get_matching(|ev| matches!(ev, EventType::ImexProgress(1)))
466                .await;
467            ctx.evtracker
468                .get_matching(|ev| matches!(ev, EventType::ImexProgress(1000)))
469                .await;
470        }
471    }
472
473    /// Tests that trying to accidentally overwrite a profile
474    /// that is in use will fail.
475    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
476    async fn test_cant_overwrite_profile_in_use() -> Result<()> {
477        let mut tcm = TestContextManager::new();
478        let ctx0 = &tcm.alice().await;
479        let ctx1 = &tcm.bob().await;
480
481        // Prepare to transfer backup.
482        let provider = BackupProvider::prepare(ctx0).await?;
483
484        // Try to overwrite an existing profile.
485        let err = get_backup(ctx1, provider.qr()).await.unwrap_err();
486        assert!(format!("{err:#}").contains("Cannot import backups to accounts in use"));
487
488        // ctx0 is supposed to also finish, and emit an error:
489        provider.await.unwrap();
490        ctx0.evtracker
491            .get_matching(|e| matches!(e, EventType::Error(_)))
492            .await;
493
494        assert_eq!(ctx1.get_primary_self_addr().await?, "bob@example.net");
495
496        Ok(())
497    }
498
499    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
500    async fn test_drop_provider() {
501        let mut tcm = TestContextManager::new();
502        let ctx = tcm.alice().await;
503
504        let provider = BackupProvider::prepare(&ctx).await.unwrap();
505        drop(provider);
506        ctx.evtracker
507            .get_matching(|ev| matches!(ev, EventType::ImexProgress(0)))
508            .await;
509    }
510}