#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_ENGINE};
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use zeroize::Zeroize;
use crate::unified_api::crypto_types::{EncryptedOutput, EncryptionScheme, HybridComponents};
use crate::unified_api::{
error::{CoreError, Result},
types::{KeyPair, SignedData},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableSignedData {
pub data: String,
pub metadata: SerializableSignedMetadata,
pub scheme: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableSignedMetadata {
pub signature: String,
pub signature_algorithm: String,
pub public_key: String,
pub key_id: Option<String>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SerializableKeyPair {
public_key: String,
private_key: String,
}
impl SerializableKeyPair {
#[must_use]
pub fn new(public_key: String, private_key: String) -> Self {
Self { public_key, private_key }
}
#[must_use]
pub fn public_key(&self) -> &str {
&self.public_key
}
#[must_use]
pub fn private_key(&self) -> &str {
&self.private_key
}
}
impl std::fmt::Debug for SerializableKeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SerializableKeyPair")
.field("public_key", &self.public_key)
.field("private_key", &"[REDACTED]")
.finish()
}
}
impl ConstantTimeEq for SerializableKeyPair {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.private_key.as_bytes().ct_eq(other.private_key.as_bytes())
}
}
impl Drop for SerializableKeyPair {
fn drop(&mut self) {
self.private_key.zeroize();
}
}
impl From<&SignedData> for SerializableSignedData {
fn from(signed: &SignedData) -> Self {
Self {
data: BASE64_ENGINE.encode(&signed.data),
metadata: SerializableSignedMetadata {
signature: BASE64_ENGINE.encode(&signed.metadata.signature),
signature_algorithm: signed.metadata.signature_algorithm.clone(),
public_key: BASE64_ENGINE.encode(&signed.metadata.public_key),
key_id: signed.metadata.key_id.clone(),
},
scheme: signed.scheme.clone(),
timestamp: signed.timestamp,
}
}
}
impl TryFrom<SerializableSignedData> for SignedData {
type Error = CoreError;
fn try_from(serializable: SerializableSignedData) -> Result<Self> {
let data = BASE64_ENGINE
.decode(&serializable.data)
.map_err(|e| CoreError::SerializationError(e.to_string()))?;
let signature = BASE64_ENGINE
.decode(&serializable.metadata.signature)
.map_err(|e| CoreError::SerializationError(e.to_string()))?;
let public_key = BASE64_ENGINE
.decode(&serializable.metadata.public_key)
.map_err(|e| CoreError::SerializationError(e.to_string()))?;
Ok(SignedData {
data,
metadata: crate::types::SignedMetadata {
signature,
signature_algorithm: serializable.metadata.signature_algorithm,
public_key,
key_id: serializable.metadata.key_id,
},
scheme: serializable.scheme,
timestamp: serializable.timestamp,
})
}
}
impl From<&KeyPair> for SerializableKeyPair {
fn from(keypair: &KeyPair) -> Self {
Self {
public_key: BASE64_ENGINE.encode(keypair.public_key().as_slice()),
private_key: BASE64_ENGINE.encode(keypair.private_key().as_slice()),
}
}
}
impl TryFrom<SerializableKeyPair> for KeyPair {
type Error = CoreError;
fn try_from(serializable: SerializableKeyPair) -> Result<Self> {
let public_key_bytes = BASE64_ENGINE
.decode(&serializable.public_key)
.map_err(|e| CoreError::SerializationError(e.to_string()))?;
let private_key_bytes = BASE64_ENGINE
.decode(&serializable.private_key)
.map_err(|e| CoreError::SerializationError(e.to_string()))?;
let public_key = crate::types::PublicKey::new(public_key_bytes);
let private_key = crate::types::PrivateKey::new(private_key_bytes);
Ok(KeyPair::new(public_key, private_key))
}
}
pub fn serialize_signed_data(signed: &SignedData) -> Result<String> {
let serializable = SerializableSignedData::from(signed);
serde_json::to_string(&serializable).map_err(|e| CoreError::SerializationError(e.to_string()))
}
pub fn deserialize_signed_data(data: &str) -> Result<SignedData> {
let serializable: SerializableSignedData =
serde_json::from_str(data).map_err(|e| CoreError::SerializationError(e.to_string()))?;
serializable.try_into()
}
pub fn serialize_keypair(keypair: &KeyPair) -> Result<String> {
let serializable = SerializableKeyPair::from(keypair);
serde_json::to_string(&serializable).map_err(|e| CoreError::SerializationError(e.to_string()))
}
pub fn deserialize_keypair(data: &str) -> Result<KeyPair> {
let serializable: SerializableKeyPair =
serde_json::from_str(data).map_err(|e| CoreError::SerializationError(e.to_string()))?;
serializable.try_into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableEncryptedOutput {
pub version: u8,
pub scheme: String,
pub ciphertext: String,
pub nonce: String,
pub tag: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hybrid_data: Option<SerializableHybridComponents>,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableHybridComponents {
pub ml_kem_ciphertext: String,
pub ecdh_ephemeral_pk: String,
}
impl From<&EncryptedOutput> for SerializableEncryptedOutput {
fn from(output: &EncryptedOutput) -> Self {
Self {
version: 2,
scheme: output.scheme().as_str().to_string(),
ciphertext: BASE64_ENGINE.encode(output.ciphertext()),
nonce: BASE64_ENGINE.encode(output.nonce()),
tag: BASE64_ENGINE.encode(output.tag()),
hybrid_data: output.hybrid_data().map(|hd| SerializableHybridComponents {
ml_kem_ciphertext: BASE64_ENGINE.encode(hd.ml_kem_ciphertext()),
ecdh_ephemeral_pk: BASE64_ENGINE.encode(hd.ecdh_ephemeral_pk()),
}),
timestamp: output.timestamp(),
key_id: output.key_id().map(str::to_owned),
}
}
}
impl TryFrom<SerializableEncryptedOutput> for EncryptedOutput {
type Error = CoreError;
fn try_from(ser: SerializableEncryptedOutput) -> Result<Self> {
const MAX_SERIALIZED_SIZE: usize = 10 * 1024 * 1024; if ser.ciphertext.len() > MAX_SERIALIZED_SIZE {
return Err(CoreError::SerializationError(format!(
"Serialized ciphertext size {} exceeds maximum of {} bytes",
ser.ciphertext.len(),
MAX_SERIALIZED_SIZE
)));
}
let scheme = EncryptionScheme::parse_str(&ser.scheme).ok_or_else(|| {
CoreError::SerializationError(format!("Unknown encryption scheme: '{}'", ser.scheme))
})?;
let ciphertext = BASE64_ENGINE.decode(&ser.ciphertext).map_err(|e| {
CoreError::SerializationError(format!("Invalid ciphertext base64: {}", e))
})?;
let nonce = BASE64_ENGINE
.decode(&ser.nonce)
.map_err(|e| CoreError::SerializationError(format!("Invalid nonce base64: {}", e)))?;
let tag = BASE64_ENGINE
.decode(&ser.tag)
.map_err(|e| CoreError::SerializationError(format!("Invalid tag base64: {}", e)))?;
let hybrid_data = ser
.hybrid_data
.map(|hd| -> Result<HybridComponents> {
let ml_kem_ciphertext =
BASE64_ENGINE.decode(&hd.ml_kem_ciphertext).map_err(|e| {
CoreError::SerializationError(format!(
"Invalid KEM ciphertext base64: {}",
e
))
})?;
let ecdh_ephemeral_pk =
BASE64_ENGINE.decode(&hd.ecdh_ephemeral_pk).map_err(|e| {
CoreError::SerializationError(format!(
"Invalid ECDH ephemeral PK base64: {}",
e
))
})?;
Ok(HybridComponents::new(ml_kem_ciphertext, ecdh_ephemeral_pk))
})
.transpose()?;
Ok(EncryptedOutput::new(
scheme,
ciphertext,
nonce,
tag,
hybrid_data,
ser.timestamp,
ser.key_id,
))
}
}
pub fn serialize_encrypted_output(output: &EncryptedOutput) -> Result<String> {
let serializable = SerializableEncryptedOutput::from(output);
serde_json::to_string(&serializable).map_err(|e| CoreError::SerializationError(e.to_string()))
}
pub fn deserialize_encrypted_output(data: &str) -> Result<EncryptedOutput> {
let serializable: SerializableEncryptedOutput =
serde_json::from_str(data).map_err(|e| CoreError::SerializationError(e.to_string()))?;
serializable.try_into()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::types::{CryptoPayload, PrivateKey, SignedMetadata};
use crate::unified_api::crypto_types::{EncryptedOutput, EncryptionScheme, HybridComponents};
fn make_signed_data() -> SignedData {
CryptoPayload {
data: vec![1, 2, 3, 4],
metadata: SignedMetadata {
signature: vec![0xBB; 64],
signature_algorithm: "ML-DSA-65".to_string(),
public_key: vec![0xCC; 32],
key_id: Some("sig-key-001".to_string()),
},
scheme: "ML-DSA-65+Ed25519".to_string(),
timestamp: 1700000002,
}
}
fn make_keypair() -> KeyPair {
KeyPair::new(crate::types::PublicKey::new(vec![1u8; 32]), PrivateKey::new(vec![2u8; 64]))
}
#[test]
fn test_signed_data_roundtrip() {
let original = make_signed_data();
let json = serialize_signed_data(&original).unwrap();
let deserialized = deserialize_signed_data(&json).unwrap();
assert_eq!(original.data, deserialized.data);
assert_eq!(original.metadata.signature, deserialized.metadata.signature);
assert_eq!(
original.metadata.signature_algorithm,
deserialized.metadata.signature_algorithm
);
assert_eq!(original.metadata.public_key, deserialized.metadata.public_key);
assert_eq!(original.metadata.key_id, deserialized.metadata.key_id);
assert_eq!(original.scheme, deserialized.scheme);
assert_eq!(original.timestamp, deserialized.timestamp);
}
#[test]
fn test_signed_data_from_trait_succeeds() {
let original = make_signed_data();
let serializable = SerializableSignedData::from(&original);
assert!(!serializable.data.is_empty());
assert_eq!(serializable.metadata.signature_algorithm, "ML-DSA-65");
}
#[test]
fn test_signed_data_try_from_invalid_base64_fails() {
let bad = SerializableSignedData {
data: "not-valid!!!".to_string(),
metadata: SerializableSignedMetadata {
signature: BASE64_ENGINE.encode(b"sig"),
signature_algorithm: "test".to_string(),
public_key: BASE64_ENGINE.encode(b"pk"),
key_id: None,
},
scheme: "test".to_string(),
timestamp: 0,
};
let result: std::result::Result<SignedData, _> = bad.try_into();
assert!(result.is_err());
}
#[test]
fn test_signed_data_try_from_invalid_signature_fails() {
let bad = SerializableSignedData {
data: BASE64_ENGINE.encode(b"data"),
metadata: SerializableSignedMetadata {
signature: "not-valid!!!".to_string(),
signature_algorithm: "test".to_string(),
public_key: BASE64_ENGINE.encode(b"pk"),
key_id: None,
},
scheme: "test".to_string(),
timestamp: 0,
};
let result: std::result::Result<SignedData, _> = bad.try_into();
assert!(result.is_err());
}
#[test]
fn test_signed_data_try_from_invalid_public_key_fails() {
let bad = SerializableSignedData {
data: BASE64_ENGINE.encode(b"data"),
metadata: SerializableSignedMetadata {
signature: BASE64_ENGINE.encode(b"sig"),
signature_algorithm: "test".to_string(),
public_key: "not-valid!!!".to_string(),
key_id: None,
},
scheme: "test".to_string(),
timestamp: 0,
};
let result: std::result::Result<SignedData, _> = bad.try_into();
assert!(result.is_err());
}
#[test]
fn test_deserialize_signed_data_invalid_json_fails() {
let result = deserialize_signed_data("not json");
assert!(result.is_err());
}
#[test]
fn test_keypair_roundtrip() {
let original = make_keypair();
let json = serialize_keypair(&original).unwrap();
let deserialized = deserialize_keypair(&json).unwrap();
assert_eq!(original.public_key(), deserialized.public_key());
assert_eq!(original.private_key().as_slice(), deserialized.private_key().as_slice());
}
#[test]
fn test_keypair_from_trait_succeeds() {
let original = make_keypair();
let serializable = SerializableKeyPair::from(&original);
assert!(!serializable.public_key.is_empty());
assert!(!serializable.private_key.is_empty());
}
#[test]
fn test_keypair_try_from_invalid_public_key_fails() {
let bad = SerializableKeyPair {
public_key: "not-valid!!!".to_string(),
private_key: BASE64_ENGINE.encode(b"secret"),
};
let result: std::result::Result<KeyPair, _> = bad.try_into();
assert!(result.is_err());
}
#[test]
fn test_keypair_try_from_invalid_private_key_fails() {
let bad = SerializableKeyPair {
public_key: BASE64_ENGINE.encode(b"public"),
private_key: "not-valid!!!".to_string(),
};
let result: std::result::Result<KeyPair, _> = bad.try_into();
assert!(result.is_err());
}
#[test]
fn test_deserialize_keypair_invalid_json_fails() {
let result = deserialize_keypair("not json");
assert!(result.is_err());
}
fn make_encrypted_output_symmetric() -> EncryptedOutput {
EncryptedOutput::new(
EncryptionScheme::Aes256Gcm,
vec![0xDE, 0xAD, 0xBE, 0xEF],
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
vec![0xAA; 16],
None,
1_700_000_000,
Some("key-001".to_string()),
)
}
fn make_encrypted_output_hybrid() -> EncryptedOutput {
EncryptedOutput::new(
EncryptionScheme::HybridMlKem768Aes256Gcm,
vec![0xBE, 0xEF, 0xCA, 0xFE],
vec![0u8; 12],
vec![0xBB; 16],
Some(HybridComponents::new(vec![0xCC; 1088], vec![0xDD; 32])),
1_700_000_001,
None,
)
}
#[test]
fn test_encrypted_output_symmetric_roundtrip() {
let original = make_encrypted_output_symmetric();
let json = serialize_encrypted_output(&original).unwrap();
let deserialized = deserialize_encrypted_output(&json).unwrap();
assert_eq!(original.scheme(), deserialized.scheme());
assert_eq!(original.ciphertext(), deserialized.ciphertext());
assert_eq!(original.nonce(), deserialized.nonce());
assert_eq!(original.tag(), deserialized.tag());
assert!(deserialized.hybrid_data().is_none());
assert_eq!(original.timestamp(), deserialized.timestamp());
assert_eq!(original.key_id(), deserialized.key_id());
}
#[test]
fn test_encrypted_output_hybrid_roundtrip() {
let original = make_encrypted_output_hybrid();
let json = serialize_encrypted_output(&original).unwrap();
let deserialized = deserialize_encrypted_output(&json).unwrap();
assert_eq!(original.scheme(), deserialized.scheme());
assert_eq!(original.ciphertext(), deserialized.ciphertext());
assert_eq!(original.nonce(), deserialized.nonce());
assert_eq!(original.tag(), deserialized.tag());
assert_eq!(original.timestamp(), deserialized.timestamp());
assert_eq!(original.key_id(), deserialized.key_id());
let orig_hd = original.hybrid_data().unwrap();
let deser_hd = deserialized.hybrid_data().unwrap();
assert_eq!(orig_hd.ml_kem_ciphertext(), deser_hd.ml_kem_ciphertext());
assert_eq!(orig_hd.ecdh_ephemeral_pk(), deser_hd.ecdh_ephemeral_pk());
}
#[test]
fn test_encrypted_output_version_field_succeeds() {
let output = make_encrypted_output_symmetric();
let json = serialize_encrypted_output(&output).unwrap();
let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(raw["version"], 2);
}
#[test]
fn test_encrypted_output_scheme_as_string_succeeds() {
let output = make_encrypted_output_hybrid();
let json = serialize_encrypted_output(&output).unwrap();
let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(raw["scheme"], "hybrid-ml-kem-768-aes-256-gcm");
}
#[test]
fn test_encrypted_output_hybrid_data_omitted_when_none_succeeds() {
let output = make_encrypted_output_symmetric();
let json = serialize_encrypted_output(&output).unwrap();
let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(raw.get("hybrid_data").is_none());
}
#[test]
fn test_encrypted_output_key_id_omitted_when_none_succeeds() {
let output = make_encrypted_output_hybrid(); let json = serialize_encrypted_output(&output).unwrap();
let raw: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(raw.get("key_id").is_none());
}
#[test]
fn test_encrypted_output_unknown_scheme_rejected_fails() {
let json = r#"{"version":2,"scheme":"fake-999","ciphertext":"AAAA","nonce":"AAAA","tag":"AAAA","timestamp":0}"#;
let result = deserialize_encrypted_output(json);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("Unknown encryption scheme"));
}
#[test]
fn test_encrypted_output_invalid_ciphertext_base64_fails() {
let json = r#"{"version":2,"scheme":"aes-256-gcm","ciphertext":"not-valid!!!","nonce":"AAAA","tag":"AAAA","timestamp":0}"#;
let result = deserialize_encrypted_output(json);
assert!(result.is_err());
}
#[test]
fn test_encrypted_output_invalid_hybrid_base64_fails() {
let json = r#"{"version":2,"scheme":"hybrid-ml-kem-768-aes-256-gcm","ciphertext":"AAAA","nonce":"AAAA","tag":"AAAA","hybrid_data":{"ml_kem_ciphertext":"not-valid!!!","ecdh_ephemeral_pk":"AAAA"},"timestamp":0}"#;
let result = deserialize_encrypted_output(json);
assert!(result.is_err());
}
#[test]
fn test_encrypted_output_invalid_json_fails() {
let result = deserialize_encrypted_output("not json");
assert!(result.is_err());
}
#[test]
fn test_encrypted_output_all_schemes_roundtrip() {
let schemes = [
EncryptionScheme::Aes256Gcm,
EncryptionScheme::ChaCha20Poly1305,
EncryptionScheme::HybridMlKem512Aes256Gcm,
EncryptionScheme::HybridMlKem768Aes256Gcm,
EncryptionScheme::HybridMlKem1024Aes256Gcm,
];
for scheme in &schemes {
let output = EncryptedOutput::new(
scheme.clone(),
vec![1, 2, 3],
vec![0u8; 12],
vec![0u8; 16],
if scheme.requires_hybrid_key() {
Some(HybridComponents::new(vec![0xAA; 32], vec![0xBB; 32]))
} else {
None
},
42,
None,
);
let json = serialize_encrypted_output(&output).unwrap();
let restored = deserialize_encrypted_output(&json).unwrap();
assert_eq!(output.scheme(), restored.scheme(), "scheme mismatch for {:?}", scheme);
}
}
#[test]
fn test_serializable_encrypted_output_clone_debug_work_correctly_succeeds() {
let output = make_encrypted_output_symmetric();
let ser = SerializableEncryptedOutput::from(&output);
let cloned = ser.clone();
assert_eq!(cloned.scheme, ser.scheme);
let debug = format!("{:?}", ser);
assert!(debug.contains("SerializableEncryptedOutput"));
}
#[test]
fn test_serializable_signed_data_clone_debug_work_correctly_succeeds() {
let original = make_signed_data();
let ser = SerializableSignedData::from(&original);
let cloned = ser.clone();
assert_eq!(cloned.scheme, ser.scheme);
let debug = format!("{:?}", ser);
assert!(debug.contains("SerializableSignedData"));
}
#[test]
fn test_serializable_keypair_clone_debug_work_correctly_succeeds() {
let original = make_keypair();
let ser = SerializableKeyPair::from(&original);
let cloned = ser.clone();
assert_eq!(cloned.public_key, ser.public_key);
let debug = format!("{:?}", ser);
assert!(debug.contains("SerializableKeyPair"));
}
}