Add OTP QR import from TOTP and entry editors (#120)
Dependency security audit / rustsec (push) Successful in 20s
Dependency security audit / rustsec (push) Successful in 20s
This commit is contained in:
1 parent
93a08e9c65
commit
3c3574b449
12 files changed
+448
-23
No files matched your search
@@ -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()?;
|
||||
|
||||
@@ -260,6 +260,12 @@ fn qr_image_import_returns_secret_bytes_and_rejects_invalid_input() -> TestResul
|
||||
image.write_to(&mut encoded, image::ImageFormat::Png)?;
|
||||
let decoded = QrMatrix::decode_image(&SecretBytes::new(encoded.into_inner()))?;
|
||||
assert_eq!(decoded.expose(), payload.expose());
|
||||
let mut oversized = Cursor::new(Vec::new());
|
||||
image::GrayImage::new(4097, 1).write_to(&mut oversized, image::ImageFormat::Png)?;
|
||||
assert!(matches!(
|
||||
QrMatrix::decode_image(&SecretBytes::new(oversized.into_inner())),
|
||||
Err(QrError::InvalidImage)
|
||||
));
|
||||
assert!(matches!(
|
||||
QrMatrix::decode_image(&SecretBytes::new(b"not an image".to_vec())),
|
||||
Err(QrError::InvalidImage)
|
||||
|
||||
Reference in new issue
Block a user