//! Storage-owned service boundary for the Iced desktop presentation adapter. use std::{ error::Error, fmt, path::Path, sync::{Arc, OnceLock}, }; use crate::{ authentication::{ AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession, }, command::{ CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, OtpInputSource, OtpInsertRequest, RemoveRequest, }, config::{Config, ConfigSettings, EditorCommand}, crypto::{KeyInfo, KeyStore, SecretProvider}, document::{ DocumentError, EntryDocument, EntryDocumentService, EntryFieldDraft, EntryFieldId, EntryFieldKind, }, git::{ AutomaticEntryCommitter, AutomaticPolicyCommitter, AutomaticTreeCommitter, GitConflict, GitConflictResolution, GitError, GitIdentity, GitOperationControl, GitProgressPhase, GitRemoteCredentialOverride, GitRepository, GitSnapshot, PullOutcome, PushOutcome, }, kdbx::{KdbxImportOutcome, KdbxImportRequest, KdbxImporter}, mutation::{MutationOutcome, TreeMutator}, otp::{OtpAlgorithm, OtpCodeValidity, OtpInput, OtpKind, OtpService, OtpUri}, presentation::{ClipboardTimeout, QrMatrix}, read::{FindResults, GrepResults, TreeModel, VaultReader}, recipient::{ PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager, RecipientPolicyOutcome, }, repository::{DirectoryPath, Repository, SecretBytes}, secret_store::SecretProtectionPolicy, write::{OverwriteDecision, VaultWriter, WriteError, WriteOutcome}, }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DesktopErrorKind { Configuration, Authentication, KeyMaterial, Repository, Read, Document, Git, Conflict, Unchanged, MissingDefaultKey, EntryExists, Mutation, Otp, Import, } #[derive(Debug)] pub struct DesktopError { kind: DesktopErrorKind, message: String, conflicts: Vec, git: Option, } impl DesktopError { pub fn kind(&self) -> DesktopErrorKind { self.kind } pub fn conflicts(&self) -> &[GitConflict] { &self.conflicts } pub fn git_error(&self) -> Option<&GitError> { self.git.as_ref() } fn new(kind: DesktopErrorKind, error: impl fmt::Display) -> Self { Self { kind, message: error.to_string(), conflicts: Vec::new(), git: None, } } fn git(error: GitError) -> Self { let (kind, conflicts) = match &error { GitError::MergeConflicts { conflicts } => { (DesktopErrorKind::Conflict, conflicts.clone()) } _ => (DesktopErrorKind::Git, Vec::new()), }; Self { kind, message: error.to_string(), conflicts, git: Some(error), } } fn document(error: DocumentError) -> Self { let kind = match &error { DocumentError::Write(WriteError::ConcurrentModification { .. }) => { DesktopErrorKind::Conflict } DocumentError::Write(WriteError::Unchanged) => DesktopErrorKind::Unchanged, _ => DesktopErrorKind::Document, }; Self::new(kind, error) } } impl fmt::Display for DesktopError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.message) } } impl Error for DesktopError {} #[derive(Clone, Debug)] pub struct DesktopStorage { config: Config, keys: Arc>, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DesktopMutationRequest { Remove(RemoveRequest), Move(MoveRequest), Copy(CopyRequest), } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DesktopGitRequest { Refresh, Pull, Push, Sync, Resolve(Vec), } impl DesktopGitRequest { pub fn requires_authentication(&self) -> bool { !matches!(self, Self::Refresh) } pub fn changes_worktree(&self) -> bool { matches!(self, Self::Pull | Self::Sync | Self::Resolve(_)) } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DesktopGitOutcome { Refreshed, Pulled(PullOutcome), Pushed(PushOutcome), Synchronized { pull: PullOutcome, push: PushOutcome, }, Resolved(PullOutcome), } #[derive(Clone, Debug)] pub struct DesktopGitResult { outcome: DesktopGitOutcome, snapshot: GitSnapshot, tree: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct DesktopOtpMetadata { kind: OtpKind, issuer: Option, account: String, algorithm: OtpAlgorithm, digits: u32, } impl DesktopOtpMetadata { pub fn kind(&self) -> OtpKind { self.kind } pub fn issuer(&self) -> Option<&str> { self.issuer.as_deref() } pub fn account(&self) -> &str { &self.account } pub fn algorithm(&self) -> OtpAlgorithm { self.algorithm } pub fn digits(&self) -> u32 { self.digits } } pub struct DesktopOtpCode { code: SecretBytes, validity: OtpCodeValidity, metadata: DesktopOtpMetadata, tree: Option, document: Option, } impl DesktopOtpCode { pub fn code(&self) -> &SecretBytes { &self.code } pub fn validity(&self) -> OtpCodeValidity { self.validity } pub fn metadata(&self) -> &DesktopOtpMetadata { &self.metadata } pub fn into_parts( self, ) -> ( SecretBytes, OtpCodeValidity, DesktopOtpMetadata, Option, Option, ) { ( self.code, self.validity, self.metadata, self.tree, self.document, ) } } pub struct DesktopOtpUri { payload: SecretBytes, matrix: Option, } impl DesktopOtpUri { pub fn payload(&self) -> &SecretBytes { &self.payload } pub fn matrix(&self) -> Option<&QrMatrix> { self.matrix.as_ref() } pub fn into_parts(self) -> (SecretBytes, Option) { (self.payload, self.matrix) } } pub struct DesktopOtpMutation { entry: String, document: EntryDocument, tree: TreeModel, } impl DesktopOtpMutation { pub fn entry(&self) -> &str { &self.entry } pub fn into_parts(self) -> (String, EntryDocument, TreeModel) { (self.entry, self.document, self.tree) } } impl DesktopGitResult { pub fn outcome(&self) -> &DesktopGitOutcome { &self.outcome } pub fn snapshot(&self) -> &GitSnapshot { &self.snapshot } pub fn into_parts(self) -> (DesktopGitOutcome, GitSnapshot, Option) { (self.outcome, self.snapshot, self.tree) } } impl DesktopMutationRequest { pub fn source(&self) -> &str { match self { Self::Remove(request) => &request.entry, Self::Move(request) => &request.source, Self::Copy(request) => &request.source, } } } impl DesktopStorage { pub fn load(explicit: Option<&Path>) -> Result { let config = Config::load(explicit) .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))?; Ok(Self { config, keys: Arc::new(OnceLock::new()), }) } pub fn system() -> Result { Self::load(None)?.bootstrap() } pub fn bootstrap(self) -> Result { let keys = self.keys()?; let handle = keys .resolve(self.config.default_key().as_str()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?; let key = keys .infos() .find(|key| key.fingerprint() == handle.fingerprint()) .ok_or_else(|| { DesktopError::new( DesktopErrorKind::MissingDefaultKey, "the configured GPG key is unavailable", ) })?; let authentication = NativeAuthenticationSession::system( SecretProtectionPolicy::default(), self.config.authentication_timeout(), ) .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; Ok(DesktopBootstrap { storage: self, authentication, key, }) } pub fn clipboard_timeout(&self) -> ClipboardTimeout { self.config.clipboard_timeout() } pub fn authentication_timeout(&self) -> AuthenticationTimeout { self.config.authentication_timeout() } pub fn vault(&self) -> &Path { self.config.vault() } pub fn config_source(&self) -> &Path { self.config.source() } pub fn default_key(&self) -> &str { self.config.default_key().as_str() } pub fn configured_editor(&self) -> Option<&EditorCommand> { self.config.configured_editor() } pub fn settings(&self) -> ConfigSettings { self.config.settings() } pub fn update_settings(&self, settings: ConfigSettings) -> Result { let storage = self.validate_settings(settings)?; storage.persist()?; Ok(storage) } pub fn update_settings_and_bootstrap( &self, settings: ConfigSettings, ) -> Result { let bootstrap = self.validate_settings(settings)?.bootstrap()?; bootstrap.storage.persist()?; Ok(bootstrap) } fn validate_settings(&self, mut settings: ConfigSettings) -> Result { let repository = Repository::open(settings.vault()) .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))?; settings.set_vault(repository.root_path().to_owned()); let config = self .config .with_settings(settings) .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))?; let storage = Self { config, keys: Arc::clone(&self.keys), }; let keys = storage.keys()?; keys.resolve(storage.config.default_key().as_str()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?; storage.tree()?; Ok(storage) } fn persist(&self) -> Result<(), DesktopError> { self.config .persist() .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error)) } /// Validate a selected folder through the same repository and key path as /// normal desktop reads, then atomically update the shared configuration. pub fn switch_vault(&self, vault: &Path) -> Result { let mut settings = self.settings(); settings.set_vault(vault.to_owned()); self.update_settings(settings) } pub fn tree(&self) -> Result { let repository = self.repository()?; let keys = self.keys()?; VaultReader::new(&repository, keys) .list(&DirectoryPath::root()) .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error)) } pub fn git_operation( &self, handle: Option<&NativeAuthenticationHandle>, request: &DesktopGitRequest, control: &GitOperationControl, ) -> Result { self.git_operation_with_ssh_passphrase(handle, request, control, None) } pub fn git_operation_with_ssh_passphrase( &self, handle: Option<&NativeAuthenticationHandle>, request: &DesktopGitRequest, control: &GitOperationControl, ssh_passphrase: Option<(&crate::config::SshFingerprint, &SecretBytes)>, ) -> Result { control .report(GitProgressPhase::Validating) .map_err(DesktopError::git)?; let repository = self.repository()?; let git = GitRepository::open(&repository, GitIdentity::ironstorage()) .map_err(DesktopError::git)?; let configured = || { self.config.git_remote(None).ok_or_else(|| { DesktopError::new( DesktopErrorKind::Configuration, "no Git remote is configured", ) }) }; let authenticated = || { let handle = handle.ok_or_else(|| { DesktopError::new( DesktopErrorKind::Authentication, "authentication is required for Git credentials", ) })?; handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; Ok(handle) }; let (outcome, changed_tree) = match request { DesktopGitRequest::Refresh => (DesktopGitOutcome::Refreshed, false), DesktopGitRequest::Pull => { let handle = authenticated()?; let credentials = ssh_passphrase.map_or_else( || GitRemoteCredentialOverride::new(handle), |(fingerprint, passphrase)| { GitRemoteCredentialOverride::with_ssh_passphrase( handle, fingerprint, passphrase, ) }, ); let outcome = git .pull_controlled(configured()?, None, &credentials, control) .map_err(DesktopError::git)?; (DesktopGitOutcome::Pulled(outcome), true) } DesktopGitRequest::Push => { let handle = authenticated()?; let credentials = ssh_passphrase.map_or_else( || GitRemoteCredentialOverride::new(handle), |(fingerprint, passphrase)| { GitRemoteCredentialOverride::with_ssh_passphrase( handle, fingerprint, passphrase, ) }, ); let outcome = git .push_controlled(configured()?, None, &credentials, control) .map_err(DesktopError::git)?; (DesktopGitOutcome::Pushed(outcome), false) } DesktopGitRequest::Sync => { let handle = authenticated()?; let credentials = ssh_passphrase.map_or_else( || GitRemoteCredentialOverride::new(handle), |(fingerprint, passphrase)| { GitRemoteCredentialOverride::with_ssh_passphrase( handle, fingerprint, passphrase, ) }, ); let (pull, push) = git .sync_controlled(configured()?, &credentials, control) .map_err(DesktopError::git)?; (DesktopGitOutcome::Synchronized { pull, push }, true) } DesktopGitRequest::Resolve(resolutions) => { let _handle = authenticated()?; control .report(GitProgressPhase::Integrating) .map_err(DesktopError::git)?; let outcome = git .resolve_fetched(configured()?, None, resolutions) .map_err(DesktopError::git)?; (DesktopGitOutcome::Resolved(outcome), true) } }; let snapshot = git .snapshot(self.config.git_remote(None), 10) .map_err(DesktopError::git)?; let tree = changed_tree.then(|| self.tree()).transpose()?; Ok(DesktopGitResult { outcome, snapshot, tree, }) } #[cfg(feature = "ssh")] pub fn confirm_ssh_host(&self, host_key: &crate::git::SshHostKey) -> Result<(), DesktopError> { let remote = self.config.git_remote(None).ok_or_else(|| { DesktopError::new( DesktopErrorKind::Configuration, "no Git remote is configured", ) })?; crate::git::confirm_ssh_host(remote, host_key).map_err(DesktopError::git) } pub fn find(&self, request: &FindRequest) -> Result { let repository = self.repository()?; let keys = self.keys()?; VaultReader::new(&repository, keys) .find(&request.terms) .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error)) } pub fn grep_active( &self, handle: &NativeAuthenticationHandle, request: &GrepRequest, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.grep(request, &mut provider) } pub fn grep( &self, request: &GrepRequest, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; VaultReader::new(&repository, keys) .grep(request, provider) .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error)) } pub fn import_kdbx_active( &self, handle: &NativeAuthenticationHandle, request: &KdbxImportRequest, password: SecretBytes, ) -> Result<(KdbxImportOutcome, TreeModel), DesktopError> { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let repository = self.repository()?; let keys = self.keys()?; let mut provider = handle.clone(); let outcome = KdbxImporter::new(&repository, keys) .import(request, password, &mut provider, GitIdentity::ironstorage()) .map_err(|error| DesktopError::new(DesktopErrorKind::Import, error))?; let tree = VaultReader::new(&repository, keys) .list(&DirectoryPath::root()) .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))?; Ok((outcome, tree)) } pub fn otp_code_active( &self, handle: &NativeAuthenticationHandle, entry: &str, unix_seconds: u64, confirm_hotp: bool, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.otp_code(entry, unix_seconds, confirm_hotp, &mut provider) } pub fn otp_code( &self, entry: &str, unix_seconds: u64, confirm_hotp: bool, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; let service = OtpService::new(&repository, keys); let uri = service .uri(entry, provider) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; if uri.kind() == OtpKind::Hotp && !confirm_hotp { return Err(DesktopError::new( DesktopErrorKind::Otp, "HOTP generation requires explicit confirmation because it commits the advanced counter", )); } let metadata = DesktopOtpMetadata { kind: uri.kind(), issuer: uri.issuer().map(str::to_owned), account: uri.account().to_owned(), algorithm: uri.algorithm(), digits: uri.digits(), }; let outcome = service .code_automatic(entry, unix_seconds, None, provider) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let validity = outcome.validity(); let changed = validity.counter().is_some(); let tree = changed.then(|| self.tree()).transpose()?; let document = changed .then(|| { EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document) }) .transpose()?; Ok(DesktopOtpCode { code: SecretBytes::new(outcome.code().expose().to_vec()), validity, metadata, tree, document, }) } pub fn otp_uri_active( &self, handle: &NativeAuthenticationHandle, entry: &str, qr: bool, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.otp_uri(entry, qr, &mut provider) } pub fn otp_uri( &self, entry: &str, qr: bool, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; let uri = OtpService::new(&repository, keys) .uri(entry, provider) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let payload = SecretBytes::new(uri.encoded().expose().to_vec()); let matrix = qr .then(|| QrMatrix::encode(&payload)) .transpose() .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; Ok(DesktopOtpUri { payload, matrix }) } pub fn import_otp_active( &self, handle: &NativeAuthenticationHandle, entry: &str, uri: SecretBytes, replace: OverwriteDecision, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.import_otp(entry, uri, replace, &mut provider) } pub fn import_otp_qr_active( &self, handle: &NativeAuthenticationHandle, entry: &str, image: SecretBytes, replace: OverwriteDecision, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.import_otp_qr(entry, image, replace, &mut provider) } pub fn import_otp_qr( &self, entry: &str, image: SecretBytes, replace: OverwriteDecision, provider: &mut impl SecretProvider, ) -> Result { let uri = QrMatrix::decode_image(&image) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; self.import_otp(entry, uri, replace, provider) } pub fn import_otp( &self, entry: &str, uri: SecretBytes, replace: OverwriteDecision, provider: &mut impl SecretProvider, ) -> Result { let parsed = OtpUri::parse(uri).map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let encoded = parsed.encoded().expose().to_vec(); let repository = self.repository()?; let keys = self.keys()?; let exists = VaultWriter::new(&repository, keys) .entry_exists(entry) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; if exists { let mut document = EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document)?; if let Some(field) = otp_field(&document)? { if replace == OverwriteDecision::Decline { return Err(DesktopError::new( DesktopErrorKind::EntryExists, format!("entry already contains an OTP URI: {entry}"), )); } document .replace_field_value(field, encoded.clone()) .map_err(DesktopError::document)?; } else { let index = document.fields().len(); document .add( index, EntryFieldDraft::otp_uri(encoded.clone()) .map_err(DesktopError::document)?, ) .map_err(DesktopError::document)?; } self.save_document(&document)?; let document = EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document)?; return Ok(DesktopOtpMutation { entry: entry.to_owned(), document, tree: self.tree()?, }); } let request = OtpInsertRequest { entry: Some(entry.to_owned()), force: false, echo: true, source: OtpInputSource::Uri, }; let input = OtpInput::line(encoded) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let service = OtpService::new(&repository, keys); let plan = service .prepare_insert(&request, input) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let mut committer = AutomaticEntryCommitter::for_entry(&repository, entry, GitIdentity::ironstorage()) .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?; service .finish_insert( plan, OverwriteDecision::Allow, OverwriteDecision::Decline, None, &mut committer, ) .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?; let document = EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document)?; Ok(DesktopOtpMutation { entry: entry.to_owned(), document, tree: self.tree()?, }) } pub fn remove_otp_active( &self, handle: &NativeAuthenticationHandle, entry: &str, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.remove_otp(entry, &mut provider) } pub fn remove_otp( &self, entry: &str, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; let mut document = EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document)?; let field = otp_field(&document)?.ok_or_else(|| { DesktopError::new( DesktopErrorKind::Otp, format!("entry does not contain an OTP URI: {entry}"), ) })?; document.remove(field).map_err(DesktopError::document)?; self.save_document(&document)?; let document = EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document)?; Ok(DesktopOtpMutation { entry: entry.to_owned(), document, tree: self.tree()?, }) } pub fn mutate_active( &self, handle: &NativeAuthenticationHandle, request: &DesktopMutationRequest, overwrite: OverwriteDecision, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.mutate(request, overwrite, &mut provider) } pub fn mutate( &self, request: &DesktopMutationRequest, overwrite: OverwriteDecision, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; let mut committer = AutomaticTreeCommitter::for_source( &repository, request.source(), GitIdentity::ironstorage(), ) .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?; let mutator = TreeMutator::new(&repository, keys); match request { DesktopMutationRequest::Remove(request) => { mutator.remove(request, overwrite, &mut committer) } DesktopMutationRequest::Move(request) => { mutator.move_tree(request, overwrite, None, provider, &mut committer) } DesktopMutationRequest::Copy(request) => { mutator.copy(request, overwrite, None, provider, &mut committer) } } .map_err(|error| DesktopError::new(DesktopErrorKind::Mutation, error)) } /// Enumerate storage-validated encryption keys for recipient selection. pub fn key_infos(&self) -> Result, DesktopError> { Ok(self.keys()?.infos().filter(KeyInfo::can_encrypt).collect()) } /// Apply an upstream-compatible root or nested `.gpg-id` policy while the /// shared authentication lease is active. pub fn apply_active_recipient_policy( &self, handle: &NativeAuthenticationHandle, request: &InitRequest, default_key: &str, ) -> Result<(Self, RecipientPolicyOutcome), DesktopError> { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.apply_recipient_policy(request, default_key, &mut provider) } pub fn apply_recipient_policy( &self, request: &InitRequest, default_key: &str, provider: &mut impl SecretProvider, ) -> Result<(Self, RecipientPolicyOutcome), DesktopError> { let repository = self.repository()?; let keys = self.keys()?; let default = keys .infos() .find(|key| key.fingerprint().as_str() == default_key) .ok_or_else(|| { DesktopError::new( DesktopErrorKind::MissingDefaultKey, "the selected default GPG key is unavailable", ) })?; if !default.has_secret() { return Err(DesktopError::new( DesktopErrorKind::MissingDefaultKey, "the selected default GPG key has no secret key material", )); } let mut settings = self.settings(); settings.set_default_key(default_key.to_owned()); let replacement = self.validate_settings(settings)?; let directory = request.path.as_deref().unwrap_or_default(); let committer = AutomaticPolicyCommitter::for_directory( &repository, directory, GitIdentity::ironstorage(), ) .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?; let mut committer = ConfigPolicyCommitter { git: committer, previous: &self.config, replacement: &replacement.config, persisted: false, }; let outcome = RecipientPolicyManager::new(&repository, keys) .apply_init(request, None, provider, &mut committer) .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))?; if !committer.persisted { replacement.persist()?; } Ok((replacement, outcome)) } /// Open a collision-free in-memory draft. No repository bytes are written /// until the structured document is explicitly saved. pub fn create_active_document( &self, handle: &NativeAuthenticationHandle, entry: &str, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; let mut provider = handle.clone(); self.create_document(entry, &mut provider) } pub fn create_document( &self, entry: &str, provider: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; if VaultWriter::new(&repository, keys) .entry_exists(entry) .map_err(|error| DesktopError::new(DesktopErrorKind::Document, error))? { return Err(DesktopError::new( DesktopErrorKind::EntryExists, format!("password-store entry already exists: {entry}"), )); } EntryDocumentService::new(&repository, keys) .open(entry, provider) .map_err(DesktopError::document) } pub fn open_document( &self, entry: &str, secrets: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; EntryDocumentService::new(&repository, keys) .open(entry, secrets) .map_err(DesktopError::document) } pub fn save_document(&self, document: &EntryDocument) -> Result { let repository = self.repository()?; let keys = self.keys()?; let entry = document.path().to_string(); let mut committer = AutomaticEntryCommitter::for_entry(&repository, &entry, GitIdentity::ironstorage()) .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?; EntryDocumentService::new(&repository, keys) .save_recoverable(document, None, &mut committer) .map_err(DesktopError::document) } pub fn save_active_document( &self, handle: &NativeAuthenticationHandle, document: &EntryDocument, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; self.save_document(document) } fn repository(&self) -> Result { Repository::open(self.config.vault()) .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error)) } fn keys(&self) -> Result<&KeyStore, DesktopError> { if let Some(keys) = self.keys.get() { return Ok(keys); } let keys = KeyStore::load(self.config.key_material()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?; let _already_initialized = self.keys.set(keys); Ok(self .keys .get() .expect("the key store was initialized by this or another caller")) } } fn otp_field(document: &EntryDocument) -> Result, DesktopError> { let mut fields = document .fields() .iter() .filter(|field| field.metadata().kind() == EntryFieldKind::OtpUri) .map(|field| field.id()); let first = fields.next(); if fields.next().is_some() { return Err(DesktopError::new( DesktopErrorKind::Otp, format!("entry contains multiple OTP URIs: {}", document.path()), )); } Ok(first) } struct ConfigPolicyCommitter<'a> { git: AutomaticPolicyCommitter, previous: &'a Config, replacement: &'a Config, persisted: bool, } impl PolicyCommitter for ConfigPolicyCommitter<'_> { fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError> { self.replacement .persist() .map_err(|error| PolicyCommitError::new(error.to_string()))?; match self.git.commit(change) { Ok(()) => { self.persisted = true; Ok(()) } Err(operation) => match self.previous.persist() { Ok(()) => Err(operation), Err(rollback) => Err(PolicyCommitError::new(format!( "recipient-policy commit failed ({operation}) and configuration rollback failed ({rollback})" ))), }, } } } pub struct DesktopBootstrap { storage: DesktopStorage, authentication: NativeAuthenticationSession, key: KeyInfo, } impl DesktopBootstrap { pub fn into_parts(self) -> (DesktopStorage, NativeAuthenticationSession, KeyInfo) { (self.storage, self.authentication, self.key) } }