diff --git a/native/native_rust_library/src/backup.rs b/native/native_rust_library/src/backup.rs index ad2767385..198c27e09 100644 --- a/native/native_rust_library/src/backup.rs +++ b/native/native_rust_library/src/backup.rs @@ -1,385 +1,385 @@ mod compaction_upload_promises; mod file_info; mod upload_handler; use crate::argon2_tools::{compute_backup_key, compute_backup_key_str}; use crate::constants::{aes, secure_store}; use crate::ffi::{ create_main_compaction, get_backup_directory_path, get_backup_user_keys_file_path, get_siwe_backup_message_path, restore_from_backup_log, restore_from_main_compaction, secure_store_get, string_callback, void_callback, }; use crate::utils::future_manager; use crate::utils::jsi_callbacks::handle_string_result_as_callback; use crate::BACKUP_SOCKET_ADDR; use crate::RUNTIME; use backup_client::{ BackupClient, BackupDescriptor, LatestBackupIDResponse, RequestedData, TryStreamExt, UserIdentity, }; use serde::{Deserialize, Serialize}; use std::error::Error; use std::path::PathBuf; pub mod ffi { use super::*; pub use upload_handler::ffi::*; pub fn create_backup( backup_id: String, backup_secret: String, pickle_key: String, pickled_account: String, siwe_backup_msg: String, promise_id: u32, ) { compaction_upload_promises::insert(backup_id.clone(), promise_id); RUNTIME.spawn(async move { let result = create_userkeys_compaction( backup_id.clone(), backup_secret, pickle_key, pickled_account, ) .await .map_err(|err| err.to_string()); if let Err(err) = result { compaction_upload_promises::resolve(&backup_id, Err(err)); return; } if !siwe_backup_msg.is_empty() { if let Err(err) = create_siwe_backup_msg_compaction(&backup_id, siwe_backup_msg).await { compaction_upload_promises::resolve(&backup_id, Err(err.to_string())); return; } } let (future_id, future) = future_manager::new_future::<()>().await; create_main_compaction(&backup_id, future_id); if let Err(err) = future.await { compaction_upload_promises::resolve(&backup_id, Err(err)); tokio::spawn(upload_handler::compaction::cleanup_files(backup_id)); return; } trigger_backup_file_upload(); // The promise will be resolved when the backup is uploaded }); } pub fn restore_backup(backup_secret: String, promise_id: u32) { RUNTIME.spawn(async move { let result = download_backup(backup_secret) .await .map_err(|err| err.to_string()); let result = match result { Ok(result) => result, Err(error) => { void_callback(error, promise_id); return; } }; let (future_id, future) = future_manager::new_future::<()>().await; restore_from_main_compaction( &result.backup_restoration_path.to_string_lossy(), &result.backup_data_key, future_id, ); if let Err(error) = future.await { void_callback(error, promise_id); return; } if let Err(error) = download_and_apply_logs(&result.backup_id, result.backup_log_data_key) .await { void_callback(error.to_string(), promise_id); return; } void_callback(String::new(), promise_id); }); } pub fn retrieve_backup_keys(backup_secret: String, promise_id: u32) { RUNTIME.spawn(async move { let result = download_backup_keys(backup_secret) .await .map_err(|err| err.to_string()); let result = match result { Ok(result) => result, Err(error) => { string_callback(error, promise_id, "".to_string()); return; } }; let serialize_result = serde_json::to_string(&result); handle_string_result_as_callback(serialize_result, promise_id); }); } pub fn restore_backup_data( backup_id: String, backup_data_key: String, backup_log_data_key: String, promise_id: u32, ) { RUNTIME.spawn(async move { let backup_keys = BackupKeysResult { backup_id, backup_data_key, backup_log_data_key, }; let result = download_backup_data(backup_keys) .await .map_err(|err| err.to_string()); let result = match result { Ok(result) => result, Err(error) => { void_callback(error, promise_id); return; } }; let (future_id, future) = future_manager::new_future::<()>().await; restore_from_main_compaction( &result.backup_restoration_path.to_string_lossy(), &result.backup_data_key, future_id, ); if let Err(error) = future.await { void_callback(error, promise_id); return; } if let Err(error) = download_and_apply_logs(&result.backup_id, result.backup_log_data_key) .await { void_callback(error.to_string(), promise_id); return; } void_callback(String::new(), promise_id); }); } } pub async fn create_userkeys_compaction( backup_id: String, backup_secret: String, pickle_key: String, pickled_account: String, ) -> Result<(), Box> { let mut backup_key = compute_backup_key(backup_secret.as_bytes(), backup_id.as_bytes())?; let backup_data_key = secure_store_get(secure_store::SECURE_STORE_ENCRYPTION_KEY_ID)?; let backup_log_data_key = secure_store_get(secure_store::SECURE_STORE_BACKUP_LOGS_ENCRYPTION_KEY_ID)?; let user_keys = UserKeys { backup_data_key, backup_log_data_key, pickle_key, pickled_account, }; let encrypted_user_keys = user_keys.encrypt(&mut backup_key)?; let user_keys_file = get_backup_user_keys_file_path(&backup_id)?; tokio::fs::write(user_keys_file, encrypted_user_keys).await?; Ok(()) } pub async fn create_siwe_backup_msg_compaction( backup_id: &String, siwe_backup_msg: String, ) -> Result<(), Box> { let siwe_backup_msg_file = get_siwe_backup_message_path(&backup_id)?; tokio::fs::write(siwe_backup_msg_file, siwe_backup_msg).await?; Ok(()) } async fn download_backup( backup_secret: String, ) -> Result> { let backup_keys = download_backup_keys(backup_secret).await?; download_backup_data(backup_keys).await } async fn download_backup_keys( backup_secret: String, ) -> Result> { let backup_client = BackupClient::new(BACKUP_SOCKET_ADDR)?; let user_identity = get_user_identity_from_secure_store()?; let latest_backup_descriptor = BackupDescriptor::Latest { username: user_identity.user_id.clone(), }; let backup_id_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupID) .await?; - let LatestBackupIDResponse { backup_id } = + let LatestBackupIDResponse { backup_id, .. } = serde_json::from_slice(&backup_id_response)?; let mut backup_key = compute_backup_key_str(&backup_secret, &backup_id)?; let mut encrypted_user_keys = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::UserKeys) .await?; let user_keys = UserKeys::from_encrypted(&mut encrypted_user_keys, &mut backup_key)?; Ok(BackupKeysResult { backup_id, backup_data_key: user_keys.backup_data_key, backup_log_data_key: user_keys.backup_log_data_key, }) } async fn download_backup_data( backup_keys: BackupKeysResult, ) -> Result> { let backup_client = BackupClient::new(BACKUP_SOCKET_ADDR)?; let user_identity = get_user_identity_from_secure_store()?; let BackupKeysResult { backup_id, backup_data_key, backup_log_data_key, } = backup_keys; let backup_data_descriptor = BackupDescriptor::BackupID { backup_id: backup_id.clone(), user_identity: user_identity.clone(), }; let encrypted_user_data = backup_client .download_backup_data(&backup_data_descriptor, RequestedData::UserData) .await?; let backup_restoration_path = PathBuf::from(get_backup_directory_path()?).join("restore_compaction"); tokio::fs::write(&backup_restoration_path, encrypted_user_data).await?; Ok(CompactionDownloadResult { backup_restoration_path, backup_id, backup_data_key, backup_log_data_key, }) } async fn download_and_apply_logs( backup_id: &str, backup_log_data_key: String, ) -> Result<(), Box> { let mut backup_log_data_key = backup_log_data_key.into_bytes(); let backup_client = BackupClient::new(BACKUP_SOCKET_ADDR)?; let user_identity = get_user_identity_from_secure_store()?; let stream = backup_client.download_logs(&user_identity, backup_id).await; let mut stream = Box::pin(stream); while let Some(mut log) = stream.try_next().await? { let data = decrypt( backup_log_data_key.as_mut_slice(), log.content.as_mut_slice(), )?; let (future_id, future) = future_manager::new_future::<()>().await; restore_from_backup_log(data, future_id); future.await?; } Ok(()) } fn get_user_identity_from_secure_store() -> Result { Ok(UserIdentity { user_id: secure_store_get(secure_store::USER_ID)?, access_token: secure_store_get(secure_store::COMM_SERVICES_ACCESS_TOKEN)?, device_id: secure_store_get(secure_store::DEVICE_ID)?, }) } #[derive(Debug, Serialize)] struct BackupKeysResult { backup_id: String, backup_data_key: String, backup_log_data_key: String, } struct CompactionDownloadResult { backup_id: String, backup_restoration_path: PathBuf, backup_data_key: String, backup_log_data_key: String, } #[derive(Debug, Serialize, Deserialize)] struct UserKeys { backup_data_key: String, backup_log_data_key: String, pickle_key: String, pickled_account: String, } impl UserKeys { fn encrypt(&self, backup_key: &mut [u8]) -> Result, Box> { let mut json = serde_json::to_vec(self)?; encrypt(backup_key, &mut json) } fn from_encrypted( data: &mut [u8], backup_key: &mut [u8], ) -> Result> { let decrypted = decrypt(backup_key, data)?; Ok(serde_json::from_slice(&decrypted)?) } } fn encrypt(key: &mut [u8], data: &mut [u8]) -> Result, Box> { let encrypted_len = data.len() + aes::IV_LENGTH + aes::TAG_LENGTH; let mut encrypted = vec![0; encrypted_len]; crate::ffi::encrypt(key, data, &mut encrypted)?; Ok(encrypted) } fn decrypt(key: &mut [u8], data: &mut [u8]) -> Result, Box> { let decrypted_len = data.len() - aes::IV_LENGTH - aes::TAG_LENGTH; let mut decrypted = vec![0; decrypted_len]; crate::ffi::decrypt(key, data, &mut decrypted)?; Ok(decrypted) } diff --git a/services/backup/src/database/backup_item.rs b/services/backup/src/database/backup_item.rs index efcf591ed..040567f5c 100644 --- a/services/backup/src/database/backup_item.rs +++ b/services/backup/src/database/backup_item.rs @@ -1,218 +1,223 @@ use crate::constants::backup_table; use aws_sdk_dynamodb::types::AttributeValue; use chrono::{DateTime, Utc}; use comm_lib::{ blob::{client::BlobServiceClient, types::BlobInfo}, database::{ AttributeExtractor, AttributeTryInto, DBItemError, TryFromAttribute, }, }; use std::collections::HashMap; #[derive(Clone, Debug)] pub struct BackupItem { pub user_id: String, pub backup_id: String, pub created: DateTime, pub user_keys: BlobInfo, pub user_data: BlobInfo, pub attachments: Vec, pub siwe_backup_msg: Option, } impl BackupItem { pub fn new( user_id: String, backup_id: String, user_keys: BlobInfo, user_data: BlobInfo, attachments: Vec, siwe_backup_msg: Option, ) -> Self { BackupItem { user_id, backup_id, created: chrono::Utc::now(), user_keys, user_data, attachments, siwe_backup_msg, } } pub fn revoke_holders(&self, blob_client: &BlobServiceClient) { blob_client.schedule_revoke_holder( &self.user_keys.blob_hash, &self.user_keys.holder, ); blob_client.schedule_revoke_holder( &self.user_data.blob_hash, &self.user_data.holder, ); for attachment_info in &self.attachments { blob_client.schedule_revoke_holder( &attachment_info.blob_hash, &attachment_info.holder, ); } } pub fn item_key( user_id: &str, backup_id: &str, ) -> HashMap { HashMap::from([ ( backup_table::attr::USER_ID.to_string(), AttributeValue::S(user_id.to_string()), ), ( backup_table::attr::BACKUP_ID.to_string(), AttributeValue::S(backup_id.to_string()), ), ]) } } impl From for HashMap { fn from(value: BackupItem) -> Self { let mut attrs = HashMap::from([ ( backup_table::attr::USER_ID.to_string(), AttributeValue::S(value.user_id), ), ( backup_table::attr::BACKUP_ID.to_string(), AttributeValue::S(value.backup_id), ), ( backup_table::attr::CREATED.to_string(), AttributeValue::S(value.created.to_rfc3339()), ), ( backup_table::attr::USER_KEYS.to_string(), value.user_keys.into(), ), ( backup_table::attr::USER_DATA.to_string(), value.user_data.into(), ), ]); if !value.attachments.is_empty() { attrs.insert( backup_table::attr::ATTACHMENTS.to_string(), AttributeValue::L( value .attachments .into_iter() .map(AttributeValue::from) .collect(), ), ); } if let Some(siwe_backup_msg_value) = value.siwe_backup_msg { attrs.insert( backup_table::attr::SIWE_BACKUP_MSG.to_string(), AttributeValue::S(siwe_backup_msg_value), ); } attrs } } impl TryFrom> for BackupItem { type Error = DBItemError; fn try_from( mut value: HashMap, ) -> Result { let user_id = String::try_from_attr( backup_table::attr::USER_ID, value.remove(backup_table::attr::USER_ID), )?; let backup_id = String::try_from_attr( backup_table::attr::BACKUP_ID, value.remove(backup_table::attr::BACKUP_ID), )?; let created = DateTime::::try_from_attr( backup_table::attr::CREATED, value.remove(backup_table::attr::CREATED), )?; let user_keys = BlobInfo::try_from_attr( backup_table::attr::USER_KEYS, value.remove(backup_table::attr::USER_KEYS), )?; let user_data = BlobInfo::try_from_attr( backup_table::attr::USER_DATA, value.remove(backup_table::attr::USER_DATA), )?; let attachments = value.remove(backup_table::attr::ATTACHMENTS); let attachments = if attachments.is_some() { attachments.attr_try_into(backup_table::attr::ATTACHMENTS)? } else { Vec::new() }; let siwe_backup_msg: Option = value.take_attr(backup_table::attr::SIWE_BACKUP_MSG)?; Ok(BackupItem { user_id, backup_id, created, user_keys, user_data, attachments, siwe_backup_msg, }) } } /// Corresponds to the items in the [`crate::constants::BACKUP_TABLE_INDEX_USERID_CREATED`] /// global index #[derive(Clone, Debug)] pub struct OrderedBackupItem { pub user_id: String, pub created: DateTime, pub backup_id: String, pub user_keys: BlobInfo, + pub siwe_backup_msg: Option, } impl TryFrom> for OrderedBackupItem { type Error = DBItemError; fn try_from( mut value: HashMap, ) -> Result { let user_id = String::try_from_attr( backup_table::attr::USER_ID, value.remove(backup_table::attr::USER_ID), )?; let created = DateTime::::try_from_attr( backup_table::attr::CREATED, value.remove(backup_table::attr::CREATED), )?; let backup_id = String::try_from_attr( backup_table::attr::BACKUP_ID, value.remove(backup_table::attr::BACKUP_ID), )?; let user_keys = BlobInfo::try_from_attr( backup_table::attr::USER_KEYS, value.remove(backup_table::attr::USER_KEYS), )?; + let siwe_backup_msg: Option = + value.take_attr(backup_table::attr::SIWE_BACKUP_MSG)?; + Ok(OrderedBackupItem { user_id, created, backup_id, user_keys, + siwe_backup_msg, }) } } diff --git a/services/backup/src/http/handlers/backup.rs b/services/backup/src/http/handlers/backup.rs index 1e976d720..bd5dcc861 100644 --- a/services/backup/src/http/handlers/backup.rs +++ b/services/backup/src/http/handlers/backup.rs @@ -1,327 +1,328 @@ use actix_web::{ error::ErrorBadRequest, web::{self, Bytes}, HttpResponse, Responder, }; use comm_lib::{ auth::UserIdentity, backup::LatestBackupIDResponse, blob::{client::BlobServiceClient, types::BlobInfo}, http::multipart::{get_named_text_field, get_text_field}, tools::Defer, }; use std::convert::Infallible; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; use tracing::{info, instrument, trace, warn}; use crate::{ database::{backup_item::BackupItem, DatabaseClient}, error::BackupError, }; #[instrument(skip_all, fields(backup_id))] pub async fn upload( user: UserIdentity, blob_client: web::Data, db_client: web::Data, mut multipart: actix_multipart::Multipart, ) -> actix_web::Result { let backup_id = get_named_text_field("backup_id", &mut multipart).await?; tracing::Span::current().record("backup_id", &backup_id); info!("Backup data upload started"); let (user_keys_blob_info, user_keys_revoke) = forward_field_to_blob( &mut multipart, &blob_client, "user_keys_hash", "user_keys", ) .await?; let (user_data_blob_info, user_data_revoke) = forward_field_to_blob( &mut multipart, &blob_client, "user_data_hash", "user_data", ) .await?; let attachments_hashes: Vec = match get_text_field(&mut multipart).await? { Some((name, attachments)) => { if name != "attachments" { warn!( name, "Malformed request: 'attachments' text field expected." ); return Err(ErrorBadRequest("Bad request")); } attachments.lines().map(ToString::to_string).collect() } None => Vec::new(), }; let mut attachments = Vec::new(); let mut attachments_revokes = Vec::new(); for attachment_hash in attachments_hashes { let (holder, revoke) = create_attachment_holder(&attachment_hash, &blob_client).await?; attachments.push(BlobInfo { blob_hash: attachment_hash, holder, }); attachments_revokes.push(revoke); } let siwe_backup_msg_option: Option = match get_text_field(&mut multipart).await? { Some((name, siwe_backup_msg)) => { if name == "siwe_backup_msg" { Some(siwe_backup_msg) } else { None } } _ => None, }; let item = BackupItem::new( user.user_id.clone(), backup_id, user_keys_blob_info, user_data_blob_info, attachments, siwe_backup_msg_option, ); db_client .put_backup_item(item) .await .map_err(BackupError::from)?; user_keys_revoke.cancel(); user_data_revoke.cancel(); for attachment_revoke in attachments_revokes { attachment_revoke.cancel(); } db_client .remove_old_backups(&user.user_id, &blob_client) .await .map_err(BackupError::from)?; Ok(HttpResponse::Ok().finish()) } #[instrument(skip_all, fields(hash_field_name, data_field_name))] async fn forward_field_to_blob<'revoke, 'blob: 'revoke>( multipart: &mut actix_multipart::Multipart, blob_client: &'blob web::Data, hash_field_name: &str, data_field_name: &str, ) -> actix_web::Result<(BlobInfo, Defer<'revoke>)> { trace!("Reading blob fields: {hash_field_name:?}, {data_field_name:?}"); let blob_hash = get_named_text_field(hash_field_name, multipart).await?; let Some(mut field) = multipart.try_next().await? else { warn!("Malformed request: expected a field."); return Err(ErrorBadRequest("Bad request"))?; }; if field.name() != data_field_name { warn!( hash_field_name, "Malformed request: '{data_field_name}' data field expected." ); return Err(ErrorBadRequest("Bad request"))?; } let blob_info = BlobInfo { blob_hash, holder: uuid::Uuid::new_v4().to_string(), }; // [`actix_multipart::Multipart`] isn't [`std::marker::Send`], and so we cannot pass it to the blob client directly. // Instead we have to forward it to a channel and create stream from the receiver. let (tx, rx) = tokio::sync::mpsc::channel(1); let receive_promise = async move { trace!("Receiving blob data"); // [`actix_multipart::MultipartError`] isn't [`std::marker::Send`] so we return it here, and pass [`Infallible`] // as the error to the channel while let Some(chunk) = field.try_next().await? { if let Err(err) = tx.send(Result::::Ok(chunk)).await { warn!("Error when sending data through a channel: '{err}'"); // Error here means that the channel has been closed from the blob client side. We don't want to return an error // here, because `tokio::try_join!` only returns the first error it receives and we want to prioritize the backup // client error. break; } } trace!("Finished receiving blob data"); Result::<(), actix_web::Error>::Ok(()) }; let data_stream = ReceiverStream::new(rx); let send_promise = async { blob_client .simple_put(&blob_info.blob_hash, &blob_info.holder, data_stream) .await .map_err(BackupError::from)?; Ok(()) }; tokio::try_join!(receive_promise, send_promise)?; let revoke_info = blob_info.clone(); let revoke_holder = Defer::new(|| { blob_client .schedule_revoke_holder(revoke_info.blob_hash, revoke_info.holder) }); Ok((blob_info, revoke_holder)) } #[instrument(skip_all)] async fn create_attachment_holder<'revoke, 'blob: 'revoke>( attachment: &str, blob_client: &'blob web::Data, ) -> Result<(String, Defer<'revoke>), BackupError> { let holder = uuid::Uuid::new_v4().to_string(); if !blob_client .assign_holder(attachment, &holder) .await .map_err(BackupError::from)? { warn!("Blob attachment with hash {attachment:?} doesn't exist"); } let revoke_hash = attachment.to_string(); let revoke_holder = holder.clone(); let revoke_holder = Defer::new(|| { blob_client.schedule_revoke_holder(revoke_hash, revoke_holder) }); Ok((holder, revoke_holder)) } #[instrument(skip_all, fields(backup_id = %path))] pub async fn download_user_keys( user: UserIdentity, path: web::Path, blob_client: web::Data, db_client: web::Data, ) -> actix_web::Result { info!("Download user keys request"); let backup_id = path.into_inner(); download_user_blob( |item| &item.user_keys, &user.user_id, &backup_id, blob_client, db_client, ) .await } #[instrument(skip_all, fields(backup_id = %path))] pub async fn download_user_data( user: UserIdentity, path: web::Path, blob_client: web::Data, db_client: web::Data, ) -> actix_web::Result { info!("Download user data request"); let backup_id = path.into_inner(); download_user_blob( |item| &item.user_data, &user.user_id, &backup_id, blob_client, db_client, ) .await } pub async fn download_user_blob( data_extractor: impl FnOnce(&BackupItem) -> &BlobInfo, user_id: &str, backup_id: &str, blob_client: web::Data, db_client: web::Data, ) -> actix_web::Result { let backup_item = db_client .find_backup_item(user_id, backup_id) .await .map_err(BackupError::from)? .ok_or(BackupError::NoBackup)?; let stream = blob_client .get(&data_extractor(&backup_item).blob_hash) .await .map_err(BackupError::from)?; Ok( HttpResponse::Ok() .content_type("application/octet-stream") .streaming(stream), ) } #[instrument(skip_all, fields(username = %path))] pub async fn get_latest_backup_id( path: web::Path, db_client: web::Data, ) -> actix_web::Result { let username = path.into_inner(); // Treat username as user_id in the initial version let user_id = username; let Some(backup_item) = db_client .find_last_backup_item(&user_id) .await .map_err(BackupError::from)? else { return Err(BackupError::NoBackup.into()); }; let response = LatestBackupIDResponse { backup_id: backup_item.backup_id, + siwe_backup_msg: backup_item.siwe_backup_msg, }; Ok(web::Json(response)) } #[instrument(skip_all, fields(username = %path))] pub async fn download_latest_backup_keys( path: web::Path, db_client: web::Data, blob_client: web::Data, ) -> actix_web::Result { let username = path.into_inner(); // Treat username as user_id in the initial version let user_id = username; let Some(backup_item) = db_client .find_last_backup_item(&user_id) .await .map_err(BackupError::from)? else { return Err(BackupError::NoBackup.into()); }; let stream = blob_client .get(&backup_item.user_keys.blob_hash) .await .map_err(BackupError::from)?; Ok( HttpResponse::Ok() .content_type("application/octet-stream") .streaming(stream), ) } diff --git a/shared/comm-lib/src/backup/mod.rs b/shared/comm-lib/src/backup/mod.rs index a681f8778..2f207af98 100644 --- a/shared/comm-lib/src/backup/mod.rs +++ b/shared/comm-lib/src/backup/mod.rs @@ -1,47 +1,50 @@ use crate::auth::UserIdentity; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct LatestBackupIDResponse { #[serde(rename = "backupID")] pub backup_id: String, + + pub siwe_backup_msg: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UploadLogRequest { pub backup_id: String, pub log_id: usize, pub content: Vec, pub attachments: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DownloadLogsRequest { pub backup_id: String, pub from_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, derive_more::From)] pub enum LogWSRequest { Authenticate(UserIdentity), UploadLog(UploadLogRequest), DownloadLogs(DownloadLogsRequest), } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum LogWSResponse { LogUploaded { backup_id: String, log_id: usize, }, LogDownload { log_id: usize, content: Vec, attachments: Option>, }, LogDownloadFinished { last_log_id: Option, }, ServerError, Unauthenticated, }