sos-migrate 0.17.1

Import and export for the Save Our Secrets SDK
Documentation
use super::{AuthenticatorUrls, OTP_AUTH_URLS};
use crate::{Error, Result};
use async_zip::{
    tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder,
    ZipEntryBuilder,
};
use sos_backend::AccessPoint;
use sos_vault::{secret::Secret, SecretAccess};
use sos_vfs as vfs;
use std::{collections::HashMap, path::Path};
use time::OffsetDateTime;
use url::Url;

/// Export an authenticator vault to a zip archive.
///
/// The access point for the vault must be unlocked.
pub async fn export_authenticator(
    path: impl AsRef<Path>,
    source: &AccessPoint,
    include_qr_codes: bool,
) -> Result<()> {
    // Gather TOTP secrets
    let mut totp_secrets = HashMap::new();
    for id in source.vault().keys() {
        if let Some((_, Secret::Totp { totp, .. }, _)) =
            source.read_secret(id).await?
        {
            totp_secrets.insert(*id, totp);
        }
    }

    let inner = vfs::File::create(path.as_ref()).await?;
    let mut writer = ZipFileWriter::with_tokio(inner);

    // Write the JSON otpauth: URLs
    let mut auth_urls = AuthenticatorUrls::default();
    for (id, totp) in &totp_secrets {
        let url: Url = totp.get_url().parse()?;
        auth_urls.otp.insert(*id, url);
    }
    let buffer = serde_json::to_vec_pretty(&auth_urls)?;
    let entry = get_entry(OTP_AUTH_URLS)?;
    writer.write_entry_whole(entry, &buffer).await?;

    if include_qr_codes {
        for (id, totp) in totp_secrets {
            let name = format!("qr/{}.png", id);
            let buffer = totp.get_qr_png().map_err(Error::Message)?;
            let entry = get_entry(&name)?;
            writer.write_entry_whole(entry, &buffer).await?;
        }
    }

    writer.close().await?;
    Ok(())
}

fn get_entry(path: &str) -> Result<ZipEntryBuilder> {
    let now = OffsetDateTime::now_utc();
    let (hours, minutes, seconds) = now.time().as_hms();
    let month: u8 = now.month().into();

    let dt = ZipDateTimeBuilder::new()
        .year(now.year().into())
        .month(month.into())
        .day(now.day().into())
        .hour(hours.into())
        .minute(minutes.into())
        .second(seconds.into())
        .build();

    Ok(ZipEntryBuilder::new(path.into(), Compression::Deflate)
        .last_modification_date(dt))
}