From b5571fcc5b296f6d4f7b690dbf4c90bff61bbe12 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Wed, 12 Aug 2026 20:11:05 +0200 Subject: [PATCH] Treat PIN fields as sensitive (#81) --- crates/storage/src/document.rs | 6 +++- crates/storage/src/mobile_entry.rs | 5 +-- crates/storage/tests/entry_documents.rs | 47 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/crates/storage/src/document.rs b/crates/storage/src/document.rs index 37a945f..4cf3b44 100644 --- a/crates/storage/src/document.rs +++ b/crates/storage/src/document.rs @@ -684,7 +684,11 @@ fn classify(index: usize, line: &[u8]) -> EntryFieldMetadata { let kind = semantic_field_kind(name); return EntryFieldMetadata { kind, - sensitivity: EntrySensitivity::Ordinary, + sensitivity: if name.eq_ignore_ascii_case("pin") { + EntrySensitivity::Sensitive + } else { + EntrySensitivity::Ordinary + }, name: Some(name.to_owned()), otp: None, diagnostic: std::str::from_utf8(&line[value_start..]) diff --git a/crates/storage/src/mobile_entry.rs b/crates/storage/src/mobile_entry.rs index 03feb27..93ecd9c 100644 --- a/crates/storage/src/mobile_entry.rs +++ b/crates/storage/src/mobile_entry.rs @@ -575,10 +575,7 @@ fn editor_field(index: usize, field: &EntryField) -> MobileEntryEditorField { | EntryFieldKind::Field => MobileEntryEditorFieldKind::Field, } }; - let sensitive = matches!( - kind, - MobileEntryEditorFieldKind::Password | MobileEntryEditorFieldKind::OtpUri - ); + let sensitive = metadata.sensitivity() == EntrySensitivity::Sensitive; MobileEntryEditorField { id: field.id().value(), kind, diff --git a/crates/storage/tests/entry_documents.rs b/crates/storage/tests/entry_documents.rs index 10c967e..759fea3 100644 --- a/crates/storage/tests/entry_documents.rs +++ b/crates/storage/tests/entry_documents.rs @@ -277,6 +277,53 @@ fn mobile_projection_marks_an_empty_first_line_without_a_reveal_control() -> Tes Ok(()) } +#[test] +fn pin_fields_are_sensitive_case_insensitively() -> TestResult { + let fixture = FixtureSet::load()?; + let store = fixture.materialize_store("basic")?; + let repository = Repository::open(store.path())?; + let keys = KeyStore::load(fixture.path("keys"))?; + let mut secrets = FixtureSecrets::all(&fixture); + write_plaintext( + &repository, + &keys, + "documents/pins", + b"password\nPIN: 1234\npin: 5678\nPiN: 9012\n", + )?; + + let document = + EntryDocumentService::new(&repository, &keys).open("documents/pins", &mut secrets)?; + let pins = &document.fields()[1..]; + assert!(pins.iter().all(|field| { + field.metadata().kind() == EntryFieldKind::Field + && field.metadata().sensitivity() == EntrySensitivity::Sensitive + })); + + let page = MobileEntryPage::from_document(&document); + let projected = page + .sections() + .iter() + .flat_map(|section| section.fields()) + .filter(|field| field.id() != 0) + .collect::>(); + assert_eq!(projected.len(), 3); + assert!(projected.iter().all(|field| { + field.sensitive() + && field.value().is_none() + && field.masked_value() == "Hidden" + && !field.selectable() + })); + assert_eq!(field_value(&document, pins[0].id().value())?, "1234"); + + let editor = MobileEntryDraft::new(document, false).page(); + assert!( + editor.fields()[1..] + .iter() + .all(|field| field.sensitive() && field.masked_value() == "Hidden") + ); + Ok(()) +} + #[test] fn named_multiline_fields_are_one_lossless_logical_field() -> TestResult { let fixture = FixtureSet::load()?;