Add OTP QR import from TOTP and entry editors (#120)
Dependency security audit / rustsec (push) Successful in 20s

This commit is contained in:
hugo committed 2026-09-12 12:06:15 +02:00
1 parent 93a08e9c65
commit 3c3574b449
12 files changed
+448 -23

No files matched your search

+81
View File
@@ -801,6 +801,87 @@ fn mobile_editor_creates_edits_generates_and_preserves_dynamic_fields() -> TestR
Ok(())
}
#[test]
fn mobile_otp_import_creates_or_appends_one_validated_field() -> TestResult {
use ironstorage::presentation::QrMatrix;
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);
let service = EntryDocumentService::new(&repository, &keys);
let uri = "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example";
let mut document = service.open("otp/new-camera", &mut secrets)?;
document.add(0, EntryFieldDraft::line(Vec::new())?)?;
let mut draft = MobileEntryDraft::new(document, true);
let original = draft.document().serialize().expose().to_vec();
assert!(
draft
.add(MobileEntryEditorFieldKind::OtpUri, None, "invalid".into())
.is_err()
);
assert_eq!(draft.document().serialize().expose(), original);
assert!(
draft
.add_otp_image(SecretBytes::new(b"not an image".to_vec()))
.is_err()
);
assert_eq!(draft.document().serialize().expose(), original);
let matrix = QrMatrix::encode(&SecretBytes::new(uri.as_bytes().to_vec()))?;
let size = (matrix.width() as u32 + 8) * 8;
let image = image::GrayImage::from_fn(size, size, |x, y| {
let dark = x / 8 >= 4
&& y / 8 >= 4
&& matrix.is_dark((x / 8 - 4) as usize, (y / 8 - 4) as usize) == Some(true);
image::Luma([if dark { 0 } else { 255 }])
});
let mut encoded = std::io::Cursor::new(Vec::new());
image.write_to(&mut encoded, image::ImageFormat::Png)?;
draft.add_otp_image(SecretBytes::new(encoded.into_inner()))?;
assert_eq!(draft.page().fields().len(), 1);
assert_eq!(
draft.page().fields()[0].kind(),
MobileEntryEditorFieldKind::OtpUri
);
assert_eq!(draft.document().serialize().expose(), uri.as_bytes());
assert!(matches!(
draft.add(MobileEntryEditorFieldKind::OtpUri, None, uri.into()),
Err(MobileEntryEditorError::ExistingOtp)
));
service.save_recoverable(draft.document(), None, &mut Committer::default())?;
let reopened = service.open("otp/new-camera", &mut secrets)?;
let otp = reopened.fields()[0].metadata().otp().expect("OTP metadata");
assert_eq!(otp.issuer(), Some("Example"));
assert_eq!(otp.account(), "alice");
write_plaintext(
&repository,
&keys,
"otp/existing",
b"keep-password\nusername: original\nnotes: keep notes\n",
)?;
let mut draft = MobileEntryDraft::new(service.open("otp/existing", &mut secrets)?, false);
let before = draft.document().serialize().expose().to_vec();
draft.add(MobileEntryEditorFieldKind::OtpUri, None, uri.into())?;
assert!(draft.document().serialize().expose().starts_with(&before));
assert_eq!(
draft.page().fields().last().unwrap().kind(),
MobileEntryEditorFieldKind::OtpUri
);
service.save_recoverable(draft.document(), None, &mut Committer::default())?;
assert_eq!(
service
.open("otp/existing", &mut secrets)?
.fields()
.last()
.unwrap()
.value(),
uri.as_bytes()
);
Ok(())
}
#[test]
fn every_compatibility_entry_round_trips_as_a_document() -> TestResult {
let fixture = FixtureSet::load()?;