sopass 0.5.0

command line password manager using SOP
Documentation
//! Password store.
//!
//! This module represents the password store and allows managing it.

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use log::info;
use serde::{Deserialize, Serialize};

use crate::{
    sop::{Certificate, Sop, SopError},
    DEFAULT_CERT_FILENAME, DEFAULT_KEY_FILENAME, DEFAULT_VALUES_FILENAME,
};

/// The password store.
///
/// This is the in-memory representation of the cleartext password
/// store. It can be updated via methods, and if modified, needs to be
/// explicitly written back to disk. Note that this module does not
/// decrypt or encrypt or do file I/O, and that those are left to the
/// caller.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Store {
    #[serde(default)]
    certs: HashMap<String, Vec<u8>>,
    kv: HashMap<String, Vec<u8>>,
}

impl Store {
    /// Add a new certificate to the store and give the certificate a
    /// name. The name can be used to access or delete the
    /// certificate. If the name of an existing certificate is used,
    /// the certificate is overwritten with a new one. When the store
    /// is encrypted, it should be encrypted with all the certificates
    /// in the store. encrypted to
    pub fn push_cert(&mut self, name: &str, cert: &Certificate) {
        self.certs.insert(name.into(), cert.as_bytes().to_vec());
    }

    /// Return iterator over all the names of certificates in the
    /// store.
    pub fn cert_names(&self) -> impl Iterator<Item = &str> {
        self.certs.keys().map(|k| k.as_str())
    }

    /// Get a named certificate if one exists.
    pub fn get_cert(&self, name: &str) -> Option<Certificate> {
        self.certs.get(name).map(|c| Certificate::new(c.to_vec()))
    }

    /// Remove a named certificate. It's OK if one doesn't exist.
    pub fn remove_cert(&mut self, name: &str) -> Result<(), StoreError> {
        if !self.certs.contains_key(name) {
            Err(StoreError::NoSuchCertificate(name.into()))
        } else if self.certs.len() == 1 {
            Err(StoreError::MustKeepOnlyCertificate)
        } else {
            self.certs.remove(name);
            Ok(())
        }
    }

    fn certs(&self) -> Vec<Certificate> {
        self.certs
            .values()
            .map(|c| Certificate::new(c.to_vec()))
            .collect()
    }

    /// Construct a [`Store`] from cleartext bytes that have been read
    /// from a file.
    pub fn from_bytes(filename: &Path, data: &[u8]) -> Result<Self, StoreError> {
        serde_json::from_slice(data).map_err(|err| StoreError::Parse(filename.into(), err))
    }

    /// Serialize a [`Store`] as bytes, to be encrypted and written to
    /// disk.
    pub fn to_bytes(&self) -> Result<Vec<u8>, StoreError> {
        serde_json::to_vec(&self).map_err(StoreError::ToJson)
    }

    /// Insert a value (secret) into the store under a given name.
    pub fn insert(&mut self, name: &str, value: &[u8]) {
        self.kv.insert(name.into(), value.into());
    }

    /// Iterate over pairs of value names and values.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
        self.kv.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
    }

    /// Get value with a given name, if any.
    pub fn get(&self, name: &str) -> Option<&[u8]> {
        self.kv.get(name).map(|v| v.as_slice())
    }

    /// Remove a value with a given name. It's OK if no such value exists.
    pub fn remove(&mut self, name: &str) {
        self.kv.remove(name);
    }

    /// Rename a value. The value must exist, and no value with the
    /// new name may exist.
    pub fn rename(&mut self, old: &str, new: &str) -> Result<(), StoreError> {
        if let Some(value) = self.kv.remove(old) {
            if self.kv.contains_key(new) {
                Err(StoreError::AlreadyExists(old.into(), new.into()))
            } else {
                self.kv.insert(new.into(), value);
                Ok(())
            }
        } else {
            Err(StoreError::DoesNotExist(old.into()))
        }
    }

    /// Import certificates and values from another store.
    pub fn import(&mut self, other: &Self) {
        for (name, cert) in other.certs.iter() {
            self.certs.insert(name.to_string(), cert.to_vec());
        }
        for (key, value) in other.kv.iter() {
            self.kv.insert(key.to_string(), value.to_vec());
        }
    }
}

/// Possible errors from using a [`Store`].
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    /// Store is not JSON.
    #[error("failed to parse store file {0} as JSON")]
    Parse(PathBuf, #[source] serde_json::Error),

    /// Can't make JSON from [`Store`].
    #[error("failed to serialize file as JSON")]
    ToJson(#[source] serde_json::Error),

    /// No value with a given name.
    #[error("can't rename value {0}: it does not exist")]
    DoesNotExist(String),

    /// Value with a given name already exists.
    #[error("can't rename value {0} to {1}: it does not exist")]
    AlreadyExists(String, String),

    /// Certificate doesn't exist.
    #[error("store does not have a certificate names {0}")]
    NoSuchCertificate(String),

    /// Can't remove only certificate.
    #[error("can't remove only certificate in store")]
    MustKeepOnlyCertificate,
}

/// An encrypted [`Store`].
///
/// This is the encrypted version of a [`Store`]. It is meant for
/// loading a store from disk and decrypting it, or encrypting a store
/// and writing it to disk.
pub struct EncryptedStore {
    values: PathBuf,
    key_file: PathBuf,
    cert_file: PathBuf,
    sop: Sop,
}

impl EncryptedStore {
    /// Create a new encrypted store.
    pub fn new(sop: Sop, dirname: &Path) -> Self {
        Self {
            values: dirname.join(DEFAULT_VALUES_FILENAME),
            key_file: dirname.join(DEFAULT_KEY_FILENAME),
            cert_file: dirname.join(DEFAULT_CERT_FILENAME),
            sop,
        }
    }

    /// Install a new private key on this device.
    pub fn install_key(&self, key_filename: &Path) -> Result<(), EncryptedStoreError> {
        copy_file(key_filename, &self.key_file)?;
        Ok(())
    }

    /// Install a new certificate identifying a private key on this
    /// device. The private key is assumed to be on an OpenPGP card.
    pub fn install_cert(&self, cert_filename: &Path) -> Result<(), EncryptedStoreError> {
        copy_file(cert_filename, &self.cert_file)?;
        Ok(())
    }

    /// Read a store from a file and decrypt it.
    pub fn read_store(&self) -> Result<Store, EncryptedStoreError> {
        info!("read and decrypt {}", self.values.display());
        let data = std::fs::read(&self.values)
            .map_err(|err| EncryptedStoreError::Load(self.values.clone(), err))?;
        let cleartext = self.sop.decrypt(data).map_err(EncryptedStoreError::Sop)?;
        Store::from_bytes(&self.values, &cleartext).map_err(EncryptedStoreError::Store)
    }

    /// Encrypt a store and write it to a file.
    pub fn write_store(&self, store: &Store) -> Result<(), EncryptedStoreError> {
        let data = store.to_bytes()?;
        self.sop
            .encrypt(data, &store.certs(), &self.values)
            .map_err(EncryptedStoreError::Sop)?;
        Ok(())
    }

    /// Extract the certificate from the private key on this device.
    pub fn extract_cert(&self) -> Result<Certificate, EncryptedStoreError> {
        self.sop.extract_cert().map_err(EncryptedStoreError::Sop)
    }
}

fn copy_file(input: &Path, store_key: &Path) -> Result<(), EncryptedStoreError> {
    info!("copy file {} to {}", input.display(), store_key.display());
    std::fs::copy(input, store_key)
        .map_err(|err| EncryptedStoreError::CopyFile(input.into(), err))?;
    Ok(())
}

/// Possible errors from using [`EncryptedStore`].
#[derive(Debug, thiserror::Error)]
pub enum EncryptedStoreError {
    /// Can't copy private key file into store.
    #[error("failed to copy file {0} to store")]
    CopyFile(PathBuf, #[source] std::io::Error),

    /// Can't run SOP.
    #[error("SOP implementation failed")]
    Sop(#[source] SopError),

    /// Can't load encrypted file.
    #[error("failed to read encrypted store {0}")]
    Load(PathBuf, #[source] std::io::Error),

    /// Can't save encrypted file.
    #[error("failed to save encrypted store as {0}")]
    Save(PathBuf, #[source] SopError),

    /// An error from [`Store`].
    #[error(transparent)]
    Store(#[from] StoreError),
}