#[cfg(feature = "serde")]
use serde::Serialize;
use time::OffsetDateTime;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(into = "json::GenericPasswordJson"))]
pub struct GenericPassword {
pub service: String,
pub account: String,
pub password: Option<Vec<u8>>,
pub description: String,
pub comment: String,
pub creator: String,
pub type_: String,
pub print_name: String,
pub alias: String,
pub created: Option<OffsetDateTime>,
pub modified: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(into = "json::InternetPasswordJson"))]
pub struct InternetPassword {
pub server: String,
pub account: String,
pub password: Option<Vec<u8>>,
pub security_domain: String,
pub protocol: String,
pub auth_type: String,
pub port: u32,
pub path: String,
pub description: String,
pub comment: String,
pub creator: String,
pub type_: String,
pub print_name: String,
pub alias: String,
pub created: Option<OffsetDateTime>,
pub modified: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(into = "json::PrivateKeyJson"))]
pub struct PrivateKey {
pub name: String,
pub data: Vec<u8>,
pub print_name: String,
pub label: String,
pub key_class: u32,
pub key_type: u32,
pub key_size: u32,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(into = "json::CertificateJson"))]
pub struct Certificate {
pub data: Vec<u8>,
pub type_: u32,
pub encoding: u32,
pub print_name: String,
pub subject: Vec<u8>,
pub issuer: Vec<u8>,
pub serial: Vec<u8>,
}
#[cfg(feature = "serde")]
mod json {
use base64::Engine as _;
use serde::Serialize;
use time::OffsetDateTime;
use super::{Certificate, GenericPassword, InternetPassword, PrivateKey};
fn b64(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
*v == T::default()
}
fn encode_secret(bytes: Option<&[u8]>) -> (String, String, String) {
match bytes {
Some(b) if !b.is_empty() => (
String::from_utf8_lossy(b).into_owned(),
hex::encode(b),
b64(b),
),
_ => (String::new(), String::new(), String::new()),
}
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) struct GenericPasswordJson {
#[serde(skip_serializing_if = "String::is_empty")]
service: String,
#[serde(skip_serializing_if = "String::is_empty")]
account: String,
#[serde(skip_serializing_if = "String::is_empty")]
password: String,
#[serde(skip_serializing_if = "String::is_empty")]
hex_password: String,
#[serde(skip_serializing_if = "String::is_empty")]
base64_password: String,
#[serde(skip_serializing_if = "String::is_empty")]
description: String,
#[serde(skip_serializing_if = "String::is_empty")]
comment: String,
#[serde(skip_serializing_if = "String::is_empty")]
creator: String,
#[serde(rename = "type", skip_serializing_if = "String::is_empty")]
type_: String,
#[serde(skip_serializing_if = "String::is_empty")]
print_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
alias: String,
#[serde(
with = "time::serde::rfc3339::option",
skip_serializing_if = "Option::is_none"
)]
created: Option<OffsetDateTime>,
#[serde(
with = "time::serde::rfc3339::option",
skip_serializing_if = "Option::is_none"
)]
modified: Option<OffsetDateTime>,
}
impl From<GenericPassword> for GenericPasswordJson {
fn from(g: GenericPassword) -> Self {
let (password, hex_password, base64_password) = encode_secret(g.password.as_deref());
Self {
service: g.service,
account: g.account,
password,
hex_password,
base64_password,
description: g.description,
comment: g.comment,
creator: g.creator,
type_: g.type_,
print_name: g.print_name,
alias: g.alias,
created: g.created,
modified: g.modified,
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) struct InternetPasswordJson {
#[serde(skip_serializing_if = "String::is_empty")]
server: String,
#[serde(skip_serializing_if = "String::is_empty")]
account: String,
#[serde(skip_serializing_if = "String::is_empty")]
password: String,
#[serde(skip_serializing_if = "String::is_empty")]
hex_password: String,
#[serde(skip_serializing_if = "String::is_empty")]
base64_password: String,
#[serde(skip_serializing_if = "String::is_empty")]
security_domain: String,
#[serde(skip_serializing_if = "String::is_empty")]
protocol: String,
#[serde(skip_serializing_if = "String::is_empty")]
auth_type: String,
#[serde(skip_serializing_if = "is_default")]
port: u32,
#[serde(skip_serializing_if = "String::is_empty")]
path: String,
#[serde(skip_serializing_if = "String::is_empty")]
description: String,
#[serde(skip_serializing_if = "String::is_empty")]
comment: String,
#[serde(skip_serializing_if = "String::is_empty")]
creator: String,
#[serde(rename = "type", skip_serializing_if = "String::is_empty")]
type_: String,
#[serde(skip_serializing_if = "String::is_empty")]
print_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
alias: String,
#[serde(
with = "time::serde::rfc3339::option",
skip_serializing_if = "Option::is_none"
)]
created: Option<OffsetDateTime>,
#[serde(
with = "time::serde::rfc3339::option",
skip_serializing_if = "Option::is_none"
)]
modified: Option<OffsetDateTime>,
}
impl From<InternetPassword> for InternetPasswordJson {
fn from(i: InternetPassword) -> Self {
let (password, hex_password, base64_password) = encode_secret(i.password.as_deref());
Self {
server: i.server,
account: i.account,
password,
hex_password,
base64_password,
security_domain: i.security_domain,
protocol: i.protocol,
auth_type: i.auth_type,
port: i.port,
path: i.path,
description: i.description,
comment: i.comment,
creator: i.creator,
type_: i.type_,
print_name: i.print_name,
alias: i.alias,
created: i.created,
modified: i.modified,
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) struct PrivateKeyJson {
#[serde(skip_serializing_if = "String::is_empty")]
name: String,
#[serde(skip_serializing_if = "String::is_empty")]
data_hex: String,
#[serde(skip_serializing_if = "String::is_empty")]
data_base64: String,
#[serde(skip_serializing_if = "String::is_empty")]
print_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
label: String,
#[serde(skip_serializing_if = "is_default")]
key_class: u32,
#[serde(skip_serializing_if = "is_default")]
key_type: u32,
#[serde(skip_serializing_if = "is_default")]
key_size: u32,
}
impl From<PrivateKey> for PrivateKeyJson {
fn from(p: PrivateKey) -> Self {
let (data_hex, data_base64) = if p.data.is_empty() {
(String::new(), String::new())
} else {
(hex::encode(&p.data), b64(&p.data))
};
Self {
name: p.name,
data_hex,
data_base64,
print_name: p.print_name,
label: p.label,
key_class: p.key_class,
key_type: p.key_type,
key_size: p.key_size,
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) struct CertificateJson {
#[serde(skip_serializing_if = "String::is_empty")]
data_hex: String,
#[serde(skip_serializing_if = "String::is_empty")]
data_base64: String,
#[serde(rename = "type", skip_serializing_if = "is_default")]
type_: u32,
#[serde(skip_serializing_if = "is_default")]
encoding: u32,
#[serde(skip_serializing_if = "String::is_empty")]
print_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
subject_hex: String,
#[serde(skip_serializing_if = "String::is_empty")]
issuer_hex: String,
#[serde(skip_serializing_if = "String::is_empty")]
serial_hex: String,
}
impl From<Certificate> for CertificateJson {
fn from(c: Certificate) -> Self {
let hex_or_empty = |b: &[u8]| {
if b.is_empty() {
String::new()
} else {
hex::encode(b)
}
};
let (data_hex, data_base64) = if c.data.is_empty() {
(String::new(), String::new())
} else {
(hex::encode(&c.data), b64(&c.data))
};
Self {
data_hex,
data_base64,
type_: c.type_,
encoding: c.encoding,
print_name: c.print_name,
subject_hex: hex_or_empty(&c.subject),
issuer_hex: hex_or_empty(&c.issuer),
serial_hex: hex_or_empty(&c.serial),
}
}
}
}
#[cfg(all(test, feature = "serde"))]
mod tests {
#![expect(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "test code uses direct unwrap and index assertions"
)]
use super::{Certificate, GenericPassword};
#[test]
fn generic_password_json_is_go_compatible() {
let gp = GenericPassword {
account: "admin".to_owned(),
password: Some(b"password#123".to_vec()),
..Default::default()
};
let v = serde_json::to_value(gp).unwrap();
assert_eq!(v["account"], "admin");
assert_eq!(v["password"], "password#123");
assert_eq!(v["hex_password"], "70617373776f726423313233");
assert_eq!(v["base64_password"], "cGFzc3dvcmQjMTIz");
}
#[test]
fn empty_password_omits_encoding_fields() {
let gp = GenericPassword {
account: "a".to_owned(),
..Default::default()
};
let v = serde_json::to_value(gp).unwrap();
assert!(v.get("password").is_none());
assert!(v.get("hex_password").is_none());
assert!(v.get("base64_password").is_none());
}
#[test]
fn certificate_json_uses_hex_blobs() {
let c = Certificate {
data: vec![0x30, 0x82],
subject: vec![0xAB],
..Default::default()
};
let v = serde_json::to_value(c).unwrap();
assert_eq!(v["data_hex"], "3082");
assert_eq!(v["subject_hex"], "ab");
}
}