feat(core): flesh out LoginCore with Zeroizing<password> and Url

Also enables zeroize's `serde` feature so Zeroizing<String> can
round-trip through serde_json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-19 12:59:46 -04:00
parent bc60f0a6b4
commit 24ed740718
3 changed files with 63 additions and 2 deletions

1
Cargo.lock generated
View File

@@ -2222,6 +2222,7 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"serde",
"zeroize_derive",
]

View File

@@ -16,7 +16,7 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
image = { version = "0.25", default-features = false, features = ["jpeg"] }
# Typed-item additions
zeroize = { version = "1", features = ["zeroize_derive"] }
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
zxcvbn = { version = "3", default-features = false }
bip39 = { version = "2", default-features = false, features = ["std"] }
unicode-normalization = "0.1"

View File

@@ -1,3 +1,63 @@
//! Login item core: username, password (Zeroizing), URL, optional TOTP.
use serde::{Deserialize, Serialize};
use url::Url;
use zeroize::Zeroizing;
use crate::item_types::TotpConfig;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LoginCore {}
pub struct LoginCore {
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<Zeroizing<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totp: Option<TotpConfig>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_login_round_trips() {
let login = LoginCore::default();
let json = serde_json::to_string(&login).unwrap();
let parsed: LoginCore = serde_json::from_str(&json).unwrap();
assert!(parsed.username.is_none());
assert!(parsed.password.is_none());
}
#[test]
fn full_login_round_trips() {
let login = LoginCore {
username: Some("alice".into()),
password: Some(Zeroizing::new("hunter2".into())),
url: Some(Url::parse("https://github.com/login").unwrap()),
totp: None,
};
let json = serde_json::to_string(&login).unwrap();
let parsed: LoginCore = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.username.as_deref(), Some("alice"));
assert_eq!(parsed.password.as_deref().map(String::as_str), Some("hunter2"));
assert_eq!(parsed.url.as_ref().map(Url::as_str), Some("https://github.com/login"));
}
#[test]
fn omitted_fields_dont_appear_in_json() {
let login = LoginCore {
username: Some("alice".into()),
password: None,
url: None,
totp: None,
};
let json = serde_json::to_string(&login).unwrap();
assert!(!json.contains("password"));
assert!(!json.contains("url"));
assert!(!json.contains("totp"));
assert!(json.contains("alice"));
}
}