Implement structured iPhone entry viewer
This commit is contained in:
@@ -9,6 +9,7 @@ use ironstorage::{
|
||||
document::{
|
||||
DocumentError, EntryDocumentService, EntryFieldDraft, EntryFieldKind, EntrySensitivity,
|
||||
},
|
||||
mobile_entry::{MobileEntryPage, MobileEntrySectionKind, field_value},
|
||||
recipient::RecipientPolicyManager,
|
||||
repository::{EntryPath, Repository, SecretBytes},
|
||||
write::{EntryCommit, EntryCommitError, EntryCommitter, WriteError},
|
||||
@@ -134,6 +135,15 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
||||
document.fields()[6].metadata().sensitivity(),
|
||||
EntrySensitivity::Sensitive
|
||||
);
|
||||
assert_eq!(
|
||||
document
|
||||
.fields()
|
||||
.iter()
|
||||
.filter(|field| field.metadata().sensitivity() == EntrySensitivity::Sensitive)
|
||||
.map(|field| field.metadata().kind())
|
||||
.collect::<Vec<_>>(),
|
||||
[EntryFieldKind::Password, EntryFieldKind::OtpUri]
|
||||
);
|
||||
let otp = document.fields()[6]
|
||||
.metadata()
|
||||
.otp()
|
||||
@@ -147,11 +157,123 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
||||
assert_eq!(otp.counter(), None);
|
||||
let copied = document.copy_field_value(document.fields()[4].id())?;
|
||||
assert_eq!(copied.expose(), b"one");
|
||||
let mobile = MobileEntryPage::from_document(&document);
|
||||
let mobile_fields = mobile
|
||||
.sections()
|
||||
.iter()
|
||||
.flat_map(|section| section.fields())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(mobile.title(), "complex");
|
||||
assert_eq!(mobile_fields.len(), document.fields().len());
|
||||
assert_eq!(mobile_fields[0].label(), "Password");
|
||||
assert!(mobile_fields[0].sensitive());
|
||||
assert_eq!(mobile_fields[0].value(), None);
|
||||
assert_eq!(mobile_fields[1].value(), Some("alice"));
|
||||
let mobile_custom = mobile_fields
|
||||
.iter()
|
||||
.filter(|field| field.label() == "Custom")
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(mobile_custom.len(), 2);
|
||||
assert_ne!(mobile_custom[0].id(), mobile_custom[1].id());
|
||||
assert_eq!(mobile_custom[0].value(), Some("one"));
|
||||
assert_eq!(mobile_custom[1].value(), Some(""));
|
||||
let mobile_otp = mobile_fields
|
||||
.iter()
|
||||
.find(|field| field.system_image() == "timer")
|
||||
.expect("OTP field");
|
||||
assert!(mobile_otp.sensitive());
|
||||
assert_eq!(mobile_otp.value(), None);
|
||||
assert!(mobile_otp.detail().is_some_and(|detail| {
|
||||
detail.contains("TOTP") && detail.contains("Example") && detail.contains("alice")
|
||||
}));
|
||||
let mobile_note = mobile_fields
|
||||
.iter()
|
||||
.find(|field| field.system_image() == "note.text")
|
||||
.expect("multiline note");
|
||||
assert!(!mobile_note.sensitive());
|
||||
assert!(mobile_note.multiline());
|
||||
assert!(mobile_note.selectable());
|
||||
assert_eq!(mobile_note.value(), Some("\r\nfirst note\r\nsecond note"));
|
||||
assert_eq!(
|
||||
field_value(&document, document.fields()[4].id().value())?,
|
||||
"one"
|
||||
);
|
||||
assert!(!format!("{document:?}").contains("pässwörd"));
|
||||
assert!(!format!("{:?}", document.fields()[6]).contains("JBSWY3DPEHPK3PXP"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_projection_preserves_partially_understood_fields_without_exposing_them() -> 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/diagnostics",
|
||||
b"password\notpauth://totp/broken\ncustom: \xff\n",
|
||||
)?;
|
||||
|
||||
let document = EntryDocumentService::new(&repository, &keys)
|
||||
.open("documents/diagnostics", &mut secrets)?;
|
||||
let page = MobileEntryPage::from_document(&document);
|
||||
let fields = page
|
||||
.sections()
|
||||
.iter()
|
||||
.flat_map(|section| section.fields())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(fields.len(), 3);
|
||||
assert_eq!(
|
||||
fields
|
||||
.iter()
|
||||
.filter(|field| field.diagnostic().is_some())
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(fields.iter().all(|field| field.value().is_none()));
|
||||
assert!(field_value(&document, document.fields()[2].id().value()).is_err());
|
||||
assert_eq!(
|
||||
document.serialize().expose(),
|
||||
b"password\notpauth://totp/broken\ncustom: \xff\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_projection_marks_an_empty_first_line_without_a_reveal_control() -> 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/empty-password",
|
||||
b"\ncomments: ordinary note\n",
|
||||
)?;
|
||||
|
||||
let document = EntryDocumentService::new(&repository, &keys)
|
||||
.open("documents/empty-password", &mut secrets)?;
|
||||
let password = document.password().expect("empty password field");
|
||||
assert_eq!(password.metadata().sensitivity(), EntrySensitivity::Empty);
|
||||
let page = MobileEntryPage::from_document(&document);
|
||||
let password = page.sections()[0].fields().first().expect("password row");
|
||||
assert_eq!(password.label(), "Password");
|
||||
assert_eq!(password.value(), None);
|
||||
assert_eq!(password.masked_value(), "Empty");
|
||||
assert!(!password.sensitive());
|
||||
assert_eq!(
|
||||
document.serialize().expose(),
|
||||
b"\ncomments: ordinary note\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_multiline_fields_are_one_lossless_logical_field() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
@@ -189,7 +311,7 @@ fn only_whitespace_free_names_end_multiline_fields() -> TestResult {
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let mut secrets = FixtureSecrets::all(&fixture);
|
||||
let plaintext = "password\ncomments: PIN: 5678\nVertragsnummer 1234\n\nwww.bahn.de Login:\n\nbenutzer / passwort\n\n(über Geschäftskundenlogin gehen!)\n\nautotype_enabled: True\nicon: 0\n";
|
||||
let plaintext = "password\ncomments: PIN: 5678\nVertragsnummer 1234\n\nwww.bahn.de Login:\n\nbenutzer / passwort\n\n(über Geschäftskundenlogin gehen!)\n\nautotype_enabled: True\nicon: 0\ntags: travel, train\n";
|
||||
write_plaintext(
|
||||
&repository,
|
||||
&keys,
|
||||
@@ -200,8 +322,12 @@ fn only_whitespace_free_names_end_multiline_fields() -> TestResult {
|
||||
let document =
|
||||
EntryDocumentService::new(&repository, &keys).open("documents/comments", &mut secrets)?;
|
||||
assert_eq!(document.serialize().expose(), plaintext.as_bytes());
|
||||
assert_eq!(document.fields().len(), 4);
|
||||
assert_eq!(document.fields().len(), 5);
|
||||
assert_eq!(document.fields()[1].metadata().name(), Some("comments"));
|
||||
assert_eq!(
|
||||
document.fields()[1].metadata().sensitivity(),
|
||||
EntrySensitivity::Ordinary
|
||||
);
|
||||
assert_eq!(
|
||||
document.fields()[1].value(),
|
||||
"PIN: 5678\nVertragsnummer 1234\n\nwww.bahn.de Login:\n\nbenutzer / passwort\n\n(über Geschäftskundenlogin gehen!)\n"
|
||||
@@ -212,6 +338,25 @@ fn only_whitespace_free_names_end_multiline_fields() -> TestResult {
|
||||
Some("autotype_enabled")
|
||||
);
|
||||
assert_eq!(document.fields()[3].metadata().name(), Some("icon"));
|
||||
assert_eq!(document.fields()[4].metadata().name(), Some("tags"));
|
||||
assert_eq!(
|
||||
document.fields()[4].metadata().sensitivity(),
|
||||
EntrySensitivity::Ordinary
|
||||
);
|
||||
let mobile = MobileEntryPage::from_document(&document);
|
||||
let comments = mobile
|
||||
.sections()
|
||||
.iter()
|
||||
.find(|section| section.kind() == MobileEntrySectionKind::Notes)
|
||||
.and_then(|section| section.fields().first())
|
||||
.expect("multiline comments section");
|
||||
assert_eq!(comments.label(), "Comments");
|
||||
assert!(!comments.sensitive());
|
||||
assert!(comments.multiline());
|
||||
assert_eq!(
|
||||
comments.value(),
|
||||
std::str::from_utf8(document.fields()[1].value()).ok()
|
||||
);
|
||||
assert!(matches!(
|
||||
EntryFieldDraft::field("www.bahn.de Login", Vec::new()),
|
||||
Err(DocumentError::InvalidFieldName)
|
||||
@@ -228,6 +373,10 @@ fn only_whitespace_free_names_end_multiline_fields() -> TestResult {
|
||||
assert_eq!(note.password().expect("password").value(), b"password");
|
||||
assert_eq!(note.fields().len(), 2);
|
||||
assert_eq!(note.fields()[1].value(), b"\nordinary note\ncontinued");
|
||||
assert_eq!(
|
||||
note.fields()[1].metadata().sensitivity(),
|
||||
EntrySensitivity::Ordinary
|
||||
);
|
||||
|
||||
let unordered = "password\nzeta: last\nnote: named\ncomments: heading\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\nordinary note\nurl: https://example.test\nlogin: alice\nalpha: first\n";
|
||||
write_plaintext(
|
||||
|
||||
Reference in New Issue
Block a user