@@ -262,6 +262,33 @@ impl EntryDocument {
|
||||
.ok_or(DocumentError::UnknownField { id })
|
||||
}
|
||||
|
||||
/// Replace only a structured field's value while preserving its storage-
|
||||
/// supplied name and syntax.
|
||||
pub fn replace_field_value(
|
||||
&mut self,
|
||||
id: EntryFieldId,
|
||||
value: Vec<u8>,
|
||||
) -> Result<(), DocumentError> {
|
||||
let field = self.field(id).ok_or(DocumentError::UnknownField { id })?;
|
||||
let draft = match field.metadata().kind() {
|
||||
EntryFieldKind::OtpUri => EntryFieldDraft::otp_uri(value)?,
|
||||
EntryFieldKind::Password | EntryFieldKind::Note | EntryFieldKind::Blank => {
|
||||
EntryFieldDraft::line(value)?
|
||||
}
|
||||
EntryFieldKind::Username
|
||||
| EntryFieldKind::Email
|
||||
| EntryFieldKind::Url
|
||||
| EntryFieldKind::Field => EntryFieldDraft::field(
|
||||
field
|
||||
.metadata()
|
||||
.name()
|
||||
.ok_or(DocumentError::InvalidFieldName)?,
|
||||
value,
|
||||
)?,
|
||||
};
|
||||
self.update(id, draft)
|
||||
}
|
||||
|
||||
pub fn conflict_token(&self) -> DocumentConflictToken {
|
||||
self.conflict_token
|
||||
}
|
||||
@@ -424,6 +451,26 @@ impl<'a> EntryDocumentService<'a> {
|
||||
committer,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Save without consuming the document, allowing an in-process editor to
|
||||
/// present a conflict or encryption failure and retain the user's draft.
|
||||
pub fn save_recoverable(
|
||||
&self,
|
||||
document: &EntryDocument,
|
||||
signing: Option<&SigningPolicy>,
|
||||
committer: &mut impl EntryCommitter,
|
||||
) -> Result<WriteOutcome, DocumentError> {
|
||||
let replacement = document.serialize();
|
||||
Ok(
|
||||
VaultWriter::new(self.repository, self.keys).finish_edit_recoverable(
|
||||
&document.session,
|
||||
replacement,
|
||||
"IronStorage structured editor",
|
||||
signing,
|
||||
committer,
|
||||
)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -46,6 +46,31 @@ impl GeneratorConfig {
|
||||
pub fn character_set(&self) -> &[char] {
|
||||
&self.character_set
|
||||
}
|
||||
|
||||
/// Generate a zeroizing password for an in-process structured editor
|
||||
/// without mutating a repository.
|
||||
pub fn generate_secret(
|
||||
&self,
|
||||
length: Option<NonZeroUsize>,
|
||||
no_symbols: bool,
|
||||
) -> Result<SecretBytes, GenerateError> {
|
||||
self.generate_secret_with_rng(length, no_symbols, &mut OsRng)
|
||||
}
|
||||
|
||||
fn generate_secret_with_rng<R: RngCore + CryptoRng>(
|
||||
&self,
|
||||
length: Option<NonZeroUsize>,
|
||||
no_symbols: bool,
|
||||
rng: &mut R,
|
||||
) -> Result<SecretBytes, GenerateError> {
|
||||
let length = validate_length(length.unwrap_or(self.default_length).get())?;
|
||||
let characters = if no_symbols {
|
||||
&self.alphanumeric_set
|
||||
} else {
|
||||
&self.character_set
|
||||
};
|
||||
generate_password(rng, length, characters)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -126,14 +151,9 @@ impl<'a> PasswordGenerator<'a> {
|
||||
if request.force && request.in_place {
|
||||
return Err(GenerateError::IncompatibleFlags);
|
||||
}
|
||||
let length = request.length.unwrap_or(self.config.default_length);
|
||||
let length = validate_length(length.get())?;
|
||||
let characters = if request.no_symbols {
|
||||
&self.config.alphanumeric_set
|
||||
} else {
|
||||
&self.config.character_set
|
||||
};
|
||||
let password = generate_password(rng, length, characters)?;
|
||||
let password =
|
||||
self.config
|
||||
.generate_secret_with_rng(request.length, request.no_symbols, rng)?;
|
||||
let path = EntryPath::parse(&request.entry)?;
|
||||
let contents = if request.in_place {
|
||||
let ciphertext = self.repository.read_entry(&path)?;
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
mutation::{TreeCommit, TreeCommitError, TreeCommitter},
|
||||
recipient::{PolicyCommit, PolicyCommitError, PolicyCommitter},
|
||||
repository::{EncryptedEntry, Repository, SecretBytes},
|
||||
write::{EntryCommit, EntryCommitError, EntryCommitter},
|
||||
write::{EntryCommit, EntryCommitError, EntryCommitter, NoGitEntryCommitter},
|
||||
};
|
||||
|
||||
const DEFAULT_BRANCH: &str = "main";
|
||||
@@ -38,6 +38,11 @@ pub struct GitIdentity {
|
||||
}
|
||||
|
||||
impl GitIdentity {
|
||||
pub fn ironstorage() -> Self {
|
||||
Self::new("IronStorage", "ironstorage@localhost")
|
||||
.expect("the built-in Git identity is valid")
|
||||
}
|
||||
|
||||
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, GitError> {
|
||||
let name = name.into();
|
||||
let email = email.into();
|
||||
@@ -257,6 +262,35 @@ pub struct GitRepository {
|
||||
identity: GitIdentity,
|
||||
}
|
||||
|
||||
/// Storage-owned selection of pass-compatible automatic Git commits.
|
||||
pub enum AutomaticEntryCommitter {
|
||||
Git(Box<GitRepository>),
|
||||
None(NoGitEntryCommitter),
|
||||
}
|
||||
|
||||
impl AutomaticEntryCommitter {
|
||||
pub fn for_entry(
|
||||
repository: &Repository,
|
||||
entry: &str,
|
||||
identity: GitIdentity,
|
||||
) -> Result<Self, GitError> {
|
||||
match GitRepository::open_innermost(repository, Path::new(entry), identity) {
|
||||
Ok(git) => Ok(Self::Git(Box::new(git))),
|
||||
Err(GitError::NotRepository) => Ok(Self::None(NoGitEntryCommitter)),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EntryCommitter for AutomaticEntryCommitter {
|
||||
fn commit(&mut self, change: &EntryCommit) -> Result<(), EntryCommitError> {
|
||||
match self {
|
||||
Self::Git(git) => EntryCommitter::commit(git.as_mut(), change),
|
||||
Self::None(committer) => committer.commit(change),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitCredential {
|
||||
username: String,
|
||||
password: crate::repository::SecretBytes,
|
||||
|
||||
@@ -337,6 +337,20 @@ impl<'a> VaultWriter<'a> {
|
||||
editor_name: &str,
|
||||
signing: Option<&SigningPolicy>,
|
||||
committer: &mut impl EntryCommitter,
|
||||
) -> Result<WriteOutcome, WriteError> {
|
||||
self.finish_edit_recoverable(&session, replacement, editor_name, signing, committer)
|
||||
}
|
||||
|
||||
/// Finish an edit while retaining the session when validation, encryption,
|
||||
/// conflict detection, or commit orchestration fails.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn finish_edit_recoverable(
|
||||
&self,
|
||||
session: &EditSession,
|
||||
replacement: SecretBytes,
|
||||
editor_name: &str,
|
||||
signing: Option<&SigningPolicy>,
|
||||
committer: &mut impl EntryCommitter,
|
||||
) -> Result<WriteOutcome, WriteError> {
|
||||
if replacement.expose() == session.plaintext.expose() {
|
||||
return Err(WriteError::Unchanged);
|
||||
@@ -375,7 +389,7 @@ impl<'a> VaultWriter<'a> {
|
||||
return Err(WriteError::Commit(error));
|
||||
}
|
||||
Ok(WriteOutcome {
|
||||
path: session.path,
|
||||
path: session.path.clone(),
|
||||
action: EntryAction::Edit,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user