keyring-manager 0.8.2

Cross-platform library for managing passwords
Documentation
use crate::error::{KeyringError, Result};
use crate::Keyring;
use crate::*;
use std::collections::HashMap;
use std::fs::{File, OpenOptions, TryLockError};
use std::io::{BufReader, BufWriter, Seek};
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::Mutex;

pub struct InsecureKeyringManagerInner {
    file: File,
    path: PathBuf,
    secrets: HashMap<(String, String, String), String>,
}

pub struct InsecureKeyringManager {
    application: String,
    inner: Mutex<InsecureKeyringManagerInner>,
}

impl InsecureKeyringManager {
    pub fn new(application: &str, path: &Path) -> Result<Self> {
        debug!(target: "keyring::insecure", "Insecure secret storage at: {:?}", path);

        // Create parent paths
        if let Some(dir) = path.parent() {
            std::fs::create_dir_all(dir)
                .map_err(map_log_err)
                .map_err(KeyringError::IoError)?;
        }

        // If our backup file exists, exit and warn the user
        let write_path = Self::get_write_path(path)?;
        if write_path.exists() {
            let errstr = format!("Backup file exists! Recover your insecure keyring file here or risk losing your data: {:?}", write_path);
            error!(target: "keyring::insecure", "{}", errstr);
            return Err(KeyringError::IoError(std::io::Error::new(
                std::io::ErrorKind::PermissionDenied,
                errstr,
            )));
        }

        // Open and lock keyring file
        let mut options = OpenOptions::new();
        let options = options.read(true).write(true).create(true);
        #[cfg(unix)]
        let options = options.mode(0o600);
        let file = options
            .open(path)
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)?;
        match file.try_lock() {
            Ok(_) => Ok(()),
            Err(TryLockError::WouldBlock) => Err(KeyringError::IoError(std::io::Error::new(
                std::io::ErrorKind::WouldBlock,
                "File is already locked",
            ))),
            Err(TryLockError::Error(e)) => Err(KeyringError::IoError(std::io::Error::new(
                std::io::ErrorKind::Other,
                e,
            ))),
        }?;

        let out = Self {
            application: application.to_owned(),
            inner: Mutex::new(InsecureKeyringManagerInner {
                file,
                path: path.to_owned(),
                secrets: HashMap::new(),
            }),
        };
        out.load_secrets()?;
        Ok(out)
    }

    pub fn with_keyring<F, T>(&self, service: &str, key: &str, func: F) -> Result<T>
    where
        F: FnOnce(&mut dyn Keyring) -> Result<T>,
    {
        let mut inner = self.inner.lock().unwrap();
        let mut kr = InsecureKeyring::new(&mut inner.secrets, &self.application, service, key)?;
        let out = func(&mut kr);
        if kr.changed {
            Self::save_secrets(&mut inner)?
        }
        out
    }

    fn get_appended_path(path: &Path, suffix: &str) -> Result<PathBuf> {
        let mut path = path.to_owned();
        let mut filename = path
            .file_name()
            .ok_or_else(|| KeyringError::Generic(format!("missing file name in path: {:?}", path)))
            .map_err(map_log_err)?
            .to_owned();
        filename.push(suffix);
        path.set_file_name(filename);
        Ok(path)
    }

    fn get_write_path(path: &Path) -> Result<PathBuf> {
        Self::get_appended_path(path, "~")
    }

    fn load_secrets(&self) -> Result<()> {
        let mut inner = self.inner.lock().unwrap();
        if inner
            .file
            .metadata()
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)?
            .len()
            == 0
        {
            return Ok(());
        }
        inner
            .file
            .rewind()
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)?;

        let buf_file = BufReader::new(
            inner
                .file
                .try_clone()
                .map_err(map_log_err)
                .map_err(KeyringError::IoError)?,
        );

        match ciborium::from_reader(buf_file)
            .map_err(map_log_err)
            .map_err(map_to_generic)
        {
            Ok(s) => {
                inner.secrets = s;
            }
            Err(_) => {
                warn!("Secrets file is corrupted, starting fresh.");
            }
        }

        Ok(())
    }

    fn save_secrets(inner: &mut InsecureKeyringManagerInner) -> Result<()> {
        debug!(target: "keyring::insecure_secrets", "Saving {} insecure secrets", inner.secrets.len());

        // Write secrets to a backup file
        let write_path = Self::get_write_path(&inner.path)?;

        let mut options = OpenOptions::new();
        let options = options.read(true).write(true).create(true).truncate(true);
        #[cfg(unix)]
        let options = options.mode(0o600);
        let mut write_file = options
            .open(&write_path)
            .map_err(KeyringError::IoError)
            .map_err(map_log_err)?;
        write_file
            .lock()
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)?;
        #[cfg(unix)]
        write_file
            .metadata()
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)?
            .permissions()
            .set_mode(0o600);

        {
            let buf_write_file = BufWriter::new(
                write_file
                    .try_clone()
                    .map_err(map_log_err)
                    .map_err(KeyringError::IoError)?,
            );

            ciborium::into_writer(&inner.secrets, buf_write_file)
                .map_err(map_log_err)
                .map_err(map_to_generic)?;
        }

        // Swap our file handle for the backup file in our structure
        std::mem::swap(&mut inner.file, &mut write_file);

        // Drop the old file handle to release the lock
        drop(write_file);

        // Now that the lock is gone, move our file over the old path
        // and we are still writing to the old
        std::fs::rename(&write_path, &inner.path)
            .map_err(map_log_err)
            .map_err(KeyringError::IoError)
    }
}

pub struct InsecureKeyring<'a> {
    secrets: &'a mut HashMap<(String, String, String), String>,
    hash_key: (String, String, String),
    changed: bool,
}

// Eventually try to get collection into the Keyring struct?
impl<'a> InsecureKeyring<'a> {
    pub fn new(
        secrets: &'a mut HashMap<(String, String, String), String>,
        application: &str,
        service: &str,
        key: &str,
    ) -> Result<InsecureKeyring<'a>> {
        Ok(InsecureKeyring {
            secrets,
            hash_key: (application.to_owned(), service.to_owned(), key.to_owned()),
            changed: false,
        })
    }
}

impl Keyring for InsecureKeyring<'_> {
    fn set_value(&mut self, value: &str) -> Result<()> {
        if let Some(old_value) = self
            .secrets
            .insert(self.hash_key.clone(), crate::escape(value).to_string())
        {
            if old_value != value {
                self.changed = true;
            }
        } else {
            self.changed = true;
        }
        Ok(())
    }

    fn get_value(&self) -> Result<String> {
        crate::unescape(
            self.secrets
                .get(&self.hash_key)
                .ok_or(KeyringError::NoPasswordFound)?,
        )
        .map_err(map_to_generic)
    }

    fn delete_value(&mut self) -> Result<()> {
        if self.secrets.remove(&self.hash_key).is_none() {
            return Err(KeyringError::NoPasswordFound);
        }
        self.changed = true;
        Ok(())
    }
}