Implement the authenticated structured entry viewer

This commit is contained in:
Hermes Agent
2026-08-10 06:45:39 +00:00
parent 57a79b7d21
commit cf7a6216ac
8 changed files with 758 additions and 28 deletions

View File

@@ -7,7 +7,7 @@ use sha2::{Digest as _, Sha256};
use crate::{
command::EditRequest,
crypto::{KeyStore, SecretProvider},
otp::OtpUri,
otp::{OtpAlgorithm, OtpKind, OtpUri},
recipient::SigningPolicy,
repository::{EntryPath, Repository, SecretBytes},
write::{EditSession, EntryCommitter, VaultWriter, WriteError, WriteOutcome},
@@ -46,6 +46,7 @@ pub struct EntryFieldMetadata {
kind: EntryFieldKind,
sensitivity: EntrySensitivity,
name: Option<String>,
otp: Option<EntryOtpMetadata>,
value: Range<usize>,
}
@@ -61,6 +62,52 @@ impl EntryFieldMetadata {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn otp(&self) -> Option<&EntryOtpMetadata> {
self.otp.as_ref()
}
}
/// Non-secret presentation metadata parsed from a validated `otpauth` URI.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryOtpMetadata {
kind: OtpKind,
issuer: Option<String>,
account: String,
algorithm: OtpAlgorithm,
digits: u32,
period: Option<u64>,
counter: Option<u64>,
}
impl EntryOtpMetadata {
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 fn period(&self) -> Option<u64> {
self.period
}
pub fn counter(&self) -> Option<u64> {
self.counter
}
}
pub struct EntryField {
@@ -205,6 +252,16 @@ impl EntryDocument {
self.fields.first()
}
/// Copy one structured field value into an independently zeroizing buffer.
///
/// Frontends use this for explicit presentation actions without reparsing
/// the pass entry or copying label syntax alongside the selected value.
pub fn copy_field_value(&self, id: EntryFieldId) -> Result<SecretBytes, DocumentError> {
self.field(id)
.map(|field| SecretBytes::new(field.value().to_vec()))
.ok_or(DocumentError::UnknownField { id })
}
pub fn conflict_token(&self) -> DocumentConflictToken {
self.conflict_token
}
@@ -440,17 +497,30 @@ fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
kind: EntryFieldKind::Password,
sensitivity: EntrySensitivity::Sensitive,
name: Some("password".to_owned()),
otp: None,
value: 0..line.len(),
};
}
if line.is_empty() {
return blank_metadata();
}
if line.starts_with(b"otpauth://") && OtpUri::parse(SecretBytes::new(line.to_vec())).is_ok() {
if line.starts_with(b"otpauth://")
&& let Ok(uri) = OtpUri::parse(SecretBytes::new(line.to_vec()))
{
let otp = EntryOtpMetadata {
kind: uri.kind(),
issuer: uri.issuer().map(str::to_owned),
account: uri.account().to_owned(),
algorithm: uri.algorithm(),
digits: uri.digits(),
period: uri.period(),
counter: uri.counter(),
};
return EntryFieldMetadata {
kind: EntryFieldKind::OtpUri,
sensitivity: EntrySensitivity::Sensitive,
name: Some("otp".to_owned()),
otp: Some(otp),
value: 0..line.len(),
};
}
@@ -465,6 +535,7 @@ fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
kind,
sensitivity: field_sensitivity(name, kind),
name: Some(name.to_owned()),
otp: None,
value: value_start..line.len(),
};
}
@@ -473,6 +544,7 @@ fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata {
kind: EntryFieldKind::Note,
sensitivity: EntrySensitivity::Sensitive,
name: None,
otp: None,
value: 0..line.len(),
}
}
@@ -504,6 +576,7 @@ fn blank_metadata() -> EntryFieldMetadata {
kind: EntryFieldKind::Blank,
sensitivity: EntrySensitivity::Empty,
name: None,
otp: None,
value: 0..0,
}
}

View File

@@ -117,6 +117,19 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
document.fields()[4].metadata().sensitivity(),
EntrySensitivity::Sensitive
);
let otp = document.fields()[4]
.metadata()
.otp()
.expect("validated OTP metadata");
assert_eq!(otp.kind(), ironstorage::otp::OtpKind::Totp);
assert_eq!(otp.issuer(), Some("Example"));
assert_eq!(otp.account(), "alice");
assert_eq!(otp.algorithm(), ironstorage::otp::OtpAlgorithm::Sha1);
assert_eq!(otp.digits(), 6);
assert_eq!(otp.period(), Some(30));
assert_eq!(otp.counter(), None);
let copied = document.copy_field_value(document.fields()[2].id())?;
assert_eq!(copied.expose(), b"one");
assert!(!format!("{document:?}").contains("pässwörd"));
assert!(!format!("{:?}", document.fields()[4]).contains("JBSWY3DPEHPK3PXP"));
Ok(())