pub mod init;
use crate::storage::credential::Credential;
use crate::storage::index::{
CredentialCache, CredentialIndexes, CredentialPathInfo, get_credential_path,
load_credential_paths, update_indexes_on_delete, update_indexes_on_write,
};
use crate::storage::{CredentialFilter, CredentialStorage};
use crate::util::{bytes_to_hex, create_secure_dir_all};
use passless_core::error::{Error, Result};
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::time::Instant;
use core::fmt;
use log::{debug, error, info, warn};
use prs_lib::crypto::IsContext;
use prs_lib::{Ciphertext, Plaintext, Store};
use zeroize::Zeroizing;
pub struct PassStorageAdapter {
store_path: PathBuf,
path: PathBuf,
gpg_backend: GpgBackend,
indexes: CredentialIndexes,
cache: CredentialCache,
iteration_index: usize,
iteration_entries: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GpgBackend {
Gpgme,
#[default]
GnupgBin,
}
impl GpgBackend {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"gpgme" => Ok(Self::Gpgme),
"gnupg-bin" | "gnupg_bin" | "gnupg" => Ok(Self::GnupgBin),
_ => Err(Error::Config(format!(
"Invalid GPG backend: '{}'. Must be 'gpgme' or 'gnupg-bin'",
s
))),
}
}
#[allow(dead_code)]
pub fn validate(&self) -> Result<()> {
match self {
Self::Gpgme => {
debug!("Validating GPGME backend configuration");
Ok(())
}
Self::GnupgBin => {
debug!("Validating GnuPG binary backend configuration");
use std::process::Command;
let result = Command::new("gpg").arg("--version").output();
match result {
Ok(output) if output.status.success() => {
debug!(
"GPG binary is available: {:?}",
String::from_utf8_lossy(&output.stdout)
);
Ok(())
}
_ => {
warn!("GPG binary not found or not executable");
Ok(()) }
}
}
}
}
}
impl Display for GpgBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GpgBackend::Gpgme => write!(f, "gpgme"),
GpgBackend::GnupgBin => write!(f, "gpg"),
}
}
}
impl PassStorageAdapter {
pub fn new(store_path: PathBuf, path: PathBuf, gpg_backend: GpgBackend) -> Result<Self> {
info!("Using pass (password-store) backend");
info!("Store path: {}", store_path.display());
info!("Path: {}", path.display());
info!("GPG backend: {}", gpg_backend);
debug!("Opening password store at: {:?}", store_path);
self::init::ensure_initialized(&store_path, gpg_backend)?;
if !store_path.exists() {
return Err(Error::Storage(format!(
"Password store path does not exist: {:?}",
store_path
)));
}
debug!("Using GPG backend: {:?}", gpg_backend);
let mut adapter = Self {
store_path,
path,
gpg_backend,
indexes: Default::default(),
cache: Default::default(),
iteration_index: Default::default(),
iteration_entries: Default::default(),
};
adapter.sync_prepare()?;
adapter.indexes = load_credential_paths(&adapter.get_fido2_path(), "gpg")
.map_err(|e| Error::Storage(format!("Failed to load credential paths: {}", e)))?;
Ok(adapter)
}
fn get_fido2_path(&self) -> PathBuf {
self.store_path.join(&self.path)
}
fn sync_prepare(&self) -> Result<()> {
debug!("Preparing password store sync");
let store = Store::open(self.store_path.to_string_lossy().as_ref()).map_err(|e| {
debug!("Failed to open store for sync: {:?}", e);
Error::Storage(format!("Failed to open store for sync: {:?}", e))
})?;
let sync = store.sync();
match sync.prepare() {
Ok(()) => {
debug!("Successfully prepared store sync (pulled if remote configured)");
Ok(())
}
Err(e) => {
warn!("Failed to prepare store sync: {:?}", e);
Ok(())
}
}
}
fn sync_finalize(&self, message: &str) -> Result<()> {
debug!("Finalizing password store sync: {}", message);
let store = Store::open(self.store_path.to_string_lossy().as_ref()).map_err(|e| {
debug!("Failed to open store for sync: {:?}", e);
Error::Storage(format!("Failed to open store for sync: {:?}", e))
})?;
let sync = store.sync();
match sync.finalize(message) {
Ok(()) => {
debug!(
"Successfully finalized store sync (committed and pushed if remote configured)"
);
Ok(())
}
Err(e) => {
debug!("Failed to finalize store sync: {:?}", e);
warn!("Failed to finalize store sync: {:?}", e);
Ok(())
}
}
}
fn create_crypto_context(&self) -> Result<prs_lib::crypto::Context> {
let proto = match self.gpg_backend {
GpgBackend::Gpgme | GpgBackend::GnupgBin => prs_lib::crypto::Proto::Gpg,
};
let config = prs_lib::crypto::Config::from(proto);
debug!("Creating crypto context with protocol: {:?}", proto);
prs_lib::crypto::context(&config).map_err(|e| {
debug!("Failed to create crypto context: {:?}", e);
Error::Storage(format!("Failed to create crypto context: {:?}", e))
})
}
#[allow(dead_code)]
fn read_credential_from_path_no_cache(&self, path: &Path) -> Result<soft_fido2::Credential> {
debug!("Reading credential (no cache) from path: {:?}", path);
let encrypted_data = std::fs::read(path).map_err(|e| {
debug!("Failed to read encrypted file: {}", e);
Error::Storage(format!("Failed to read file: {}", e))
})?;
let mut context = self.create_crypto_context()?;
let ciphertext = Ciphertext::from(encrypted_data);
let plaintext = context.decrypt(ciphertext).map_err(|e| {
error!("Failed to decrypt credential: {:?}", e);
Error::Storage(format!("Failed to decrypt credential: {:?}", e))
})?;
debug!("Successfully decrypted credential");
Credential::from_bytes(plaintext.unsecure_ref())
.map(|cred| cred.to_soft_fido2())
.map_err(|e| Error::Storage(format!("Failed to parse credential: {:?}", e)))
}
fn read_credential_from_path(&mut self, path: &Path) -> Result<soft_fido2::Credential> {
if let Some(cached) = self.cache.get(path) {
if Instant::now() < cached.expires_at {
debug!("Cache HIT for path: {:?}", path);
return Ok(cached.credential.clone());
} else {
debug!("Cache entry expired for path: {:?}", path);
}
}
debug!(
"Cache MISS - reading and decrypting credential from path: {:?}",
path
);
self.cache.evict_expired();
self.cache.evict_oldest_if_full();
let encrypted_data = std::fs::read(path).map_err(|e| {
debug!("Failed to read encrypted file: {}", e);
Error::Storage(format!("Failed to read file: {}", e))
})?;
let mut context = self.create_crypto_context()?;
let ciphertext = Ciphertext::from(encrypted_data);
let plaintext = context.decrypt(ciphertext).map_err(|e| {
error!("Failed to decrypt credential: {:?}", e);
Error::Storage(format!("Failed to decrypt credential: {:?}", e))
})?;
debug!("Successfully decrypted credential");
let credential: soft_fido2::Credential = Credential::from_bytes(plaintext.unsecure_ref())
.map(|cred| cred.to_soft_fido2())
.map_err(|e| Error::Storage(format!("Failed to parse credential: {:?}", e)))?;
self.cache.insert(path.to_path_buf(), credential.clone());
Ok(credential)
}
fn read_credential_by_id(&mut self, id: &[u8]) -> Result<soft_fido2::Credential> {
let path_info = self.indexes.id.get(id).ok_or_else(|| {
debug!("Credential not found in index");
Error::Storage("Credential not found".to_string())
})?;
let path = path_info.to_path(&self.get_fido2_path());
self.read_credential_from_path(&path)
}
fn load_recipients_from_gpg_id(&self) -> Result<prs_lib::Recipients> {
let gpg_id_path = &self.store_path.join(".gpg-id");
debug!("Checking for .gpg-id at: {:?}", gpg_id_path);
match std::fs::read_to_string(gpg_id_path) {
Ok(content) => {
debug!("Found .gpg-id at: {:?}", gpg_id_path);
self.parse_gpg_id_content(&content, gpg_id_path)
}
Err(e) => {
debug!("Failed to read .gpg-id at {:?}: {}", gpg_id_path, e);
Err(Error::Storage(format!(
"Failed to find .gpg-id file in {:?} or any parent directory. Make sure the password store is initialized with: pass init <gpg-key-id>",
self.store_path
)))
}
}
}
fn parse_gpg_id_content(
&self,
content: &str,
gpg_id_path: &Path,
) -> Result<prs_lib::Recipients> {
let keys: Vec<prs_lib::Key> = content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(|key_id| {
debug!("Found GPG key ID: {}", key_id);
let gpg_key = prs_lib::crypto::proto::gpg::Key {
fingerprint: key_id.to_string(),
user_ids: vec![],
};
prs_lib::Key::Gpg(gpg_key)
})
.collect();
if keys.is_empty() {
return Err(Error::Storage(format!(
"No GPG key IDs found in .gpg-id file at {:?}",
gpg_id_path
)));
}
debug!(
"Loaded {} GPG recipient(s) from {:?}",
keys.len(),
gpg_id_path
);
Ok(prs_lib::Recipients::from(keys))
}
fn write_credential_bytes(
&mut self,
cred: &soft_fido2::Credential,
cred_bytes: &[u8],
) -> Result<()> {
self.cache.evict_expired();
let path = get_credential_path(&self.get_fido2_path(), &cred.rp.id, &cred.id, "gpg");
debug!("Writing credential to: {:?}", path);
if let Some(parent) = path.parent() {
create_secure_dir_all(parent).map_err(|e| {
debug!("Failed to create directory: {}", e);
Error::Storage(format!("Failed to create directory: {}", e))
})?;
}
let recipients = self.load_recipients_from_gpg_id()?;
let mut context = self.create_crypto_context()?;
let plaintext = Plaintext::from(cred_bytes.to_vec());
context
.encrypt_file(&recipients, plaintext, &path)
.map_err(|e| {
debug!("Failed to encrypt credential: {:?}", e);
Error::Storage(format!("Failed to encrypt credential: {:?}", e))
})?;
debug!("Successfully wrote and encrypted credential");
self.cache.remove(&path);
let path_info =
CredentialPathInfo::new(cred.rp.id.clone(), cred.id.to_vec(), "gpg".to_string());
update_indexes_on_write(&mut self.indexes, path_info);
let relative_path = path
.strip_prefix(&self.store_path)
.unwrap_or(&path)
.display();
let commit_message = format!("Add generated password for {}.", relative_path);
self.sync_finalize(&commit_message)?;
Ok(())
}
fn delete_credential(&mut self, id: &[u8]) -> Result<()> {
self.cache.evict_expired();
debug!("Deleting credential with ID: {}", bytes_to_hex(id));
let path_info = self
.indexes
.id
.get(id)
.ok_or_else(|| {
debug!("Credential not found in index");
Error::Storage("Credential not found".to_string())
})?
.clone();
let path = path_info.to_path(&self.get_fido2_path());
std::fs::remove_file(&path).map_err(|e| {
debug!("Failed to delete file: {}", e);
Error::Storage(format!("Failed to delete file: {}", e))
})?;
self.cache.remove(&path);
update_indexes_on_delete(&mut self.indexes, id);
debug!("Successfully deleted credential");
let relative_path = path
.strip_prefix(&self.store_path)
.unwrap_or(&path)
.display();
let commit_message = format!("Remove {} from store.", relative_path);
self.sync_finalize(&commit_message)?;
Ok(())
}
fn find_next(&mut self) -> Result<soft_fido2::Credential> {
debug!(
"Finding next credential (index: {}/{})",
self.iteration_index,
self.iteration_entries.len()
);
if self.iteration_index >= self.iteration_entries.len() {
debug!("No more credentials matching filter");
return Err(Error::Storage("No more credentials".to_string()));
}
let path = self.iteration_entries[self.iteration_index].clone();
self.iteration_index += 1;
self.read_credential_from_path(&path)
}
}
impl CredentialStorage for PassStorageAdapter {
fn read_first(
&mut self,
filter: CredentialFilter,
) -> soft_fido2::Result<soft_fido2::Credential> {
self.cache.evict_expired();
debug!("read_first called with filter: {:?}", filter);
self.iteration_entries = self.indexes.resolve_filter(&filter, &self.get_fido2_path());
self.iteration_index = 0;
debug!(
"Starting iteration with {} entries for filter: {:?}",
self.iteration_entries.len(),
filter
);
self.find_next().map_err(Into::into)
}
fn read_next(&mut self) -> soft_fido2::Result<soft_fido2::Credential> {
self.cache.evict_expired();
debug!("read_next called");
self.find_next().map_err(Into::into)
}
fn read(&mut self, id: &[u8]) -> soft_fido2::Result<soft_fido2::Credential> {
self.cache.evict_expired();
debug!("read called with id: {}", bytes_to_hex(id));
self.read_credential_by_id(id).map_err(Into::into)
}
fn write(&mut self, cred_ref: soft_fido2::CredentialRef) -> soft_fido2::Result<()> {
self.cache.evict_expired();
debug!("write called for RP: {}", cred_ref.rp_id);
let credential = cred_ref.to_owned();
let our_cred = Credential::from_soft_fido2(&credential);
let cred_bytes = Zeroizing::new(our_cred.to_bytes().map_err(|e| {
debug!("Failed to serialize credential: {:?}", e);
Error::Storage(format!("Failed to serialize credential: {:?}", e))
})?);
self.write_credential_bytes(&credential, &cred_bytes)
.map_err(Into::into)
}
fn delete(&mut self, id: &[u8]) -> soft_fido2::Result<()> {
self.cache.evict_expired();
debug!("delete called with id: {}", bytes_to_hex(id));
self.delete_credential(id).map_err(Into::into)
}
fn count_credentials(&self) -> usize {
let count = self.indexes.id.len();
debug!("count_credentials: {}", count);
count
}
fn disable_user_verification(&self) -> bool {
true
}
fn cleanup_expired_cache(&mut self) {
self.cache.evict_expired();
}
}