use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use serde::{Deserialize, Serialize};
use crate::keys::KeyType;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidSecretsBundle {
pub did: String,
pub secrets: Vec<SecretEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretEntry {
pub key_id: String,
pub key_type: KeyType,
pub private_key_multibase: String,
}
#[cfg(feature = "client")]
impl From<crate::client::GetKeySecretResponse> for SecretEntry {
fn from(resp: crate::client::GetKeySecretResponse) -> Self {
Self {
key_id: resp.key_id,
key_type: resp.key_type,
private_key_multibase: resp.private_key_multibase,
}
}
}
impl DidSecretsBundle {
pub fn decode(encoded: &str) -> Result<Self, DidSecretsBundleError> {
let json_bytes = BASE64
.decode(encoded)
.map_err(|e| DidSecretsBundleError::Base64(e.to_string()))?;
serde_json::from_slice(&json_bytes).map_err(|e| DidSecretsBundleError::Json(e.to_string()))
}
pub fn encode(&self) -> Result<String, DidSecretsBundleError> {
let json =
serde_json::to_vec(self).map_err(|e| DidSecretsBundleError::Json(e.to_string()))?;
Ok(BASE64.encode(&json))
}
}
#[derive(Debug)]
pub enum DidSecretsBundleError {
Base64(String),
Json(String),
}
impl std::fmt::Display for DidSecretsBundleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Base64(e) => write!(f, "base64 decode error: {e}"),
Self::Json(e) => write!(f, "JSON error: {e}"),
}
}
}
impl std::error::Error for DidSecretsBundleError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let bundle = DidSecretsBundle {
did: "did:webvh:abc123:example.com".to_string(),
secrets: vec![
SecretEntry {
key_id: "did:webvh:abc123:example.com#key-0".to_string(),
key_type: KeyType::Ed25519,
private_key_multibase: "z6Mk...signing".to_string(),
},
SecretEntry {
key_id: "did:webvh:abc123:example.com#key-1".to_string(),
key_type: KeyType::X25519,
private_key_multibase: "z6Mk...ka".to_string(),
},
],
};
let encoded = bundle.encode().unwrap();
let decoded = DidSecretsBundle::decode(&encoded).unwrap();
assert_eq!(decoded.did, bundle.did);
assert_eq!(decoded.secrets.len(), 2);
assert_eq!(decoded.secrets[0].key_id, bundle.secrets[0].key_id);
assert_eq!(decoded.secrets[0].key_type, KeyType::Ed25519);
assert_eq!(
decoded.secrets[0].private_key_multibase,
bundle.secrets[0].private_key_multibase
);
assert_eq!(decoded.secrets[1].key_type, KeyType::X25519);
}
#[test]
fn test_decode_invalid_base64() {
let result = DidSecretsBundle::decode("!!!not-base64!!!");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("base64"));
}
#[test]
fn test_decode_invalid_json() {
let encoded = BASE64.encode(b"not json");
let result = DidSecretsBundle::decode(&encoded);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("JSON"));
}
#[test]
fn test_decode_empty_secrets() {
let bundle = DidSecretsBundle {
did: "did:example:123".to_string(),
secrets: vec![],
};
let encoded = bundle.encode().unwrap();
let decoded = DidSecretsBundle::decode(&encoded).unwrap();
assert_eq!(decoded.did, "did:example:123");
assert!(decoded.secrets.is_empty());
}
}