vta-sdk 0.4.1

SDK for Verifiable Trust Agents operating in Verifiable Trust Communities
Documentation
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use serde::{Deserialize, Serialize};

use crate::keys::KeyType;

/// A portable bundle of DID secrets for import/export.
///
/// Encodes as JSON, then base64url-no-pad for safe transport.
///
/// # Example — decoding
///
/// ```
/// use vta_sdk::did_secrets::DidSecretsBundle;
///
/// let bundle = DidSecretsBundle::decode("eyJkaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJzZWNyZXRzIjpbXX0")
///     .expect("valid bundle");
/// assert_eq!(bundle.did, "did:example:123");
/// ```
///
/// # Example — constructing secrets for DIDComm
///
/// Applications using `affinidi_tdk` can reconstruct `Secret` objects from the
/// entries in this bundle using `Secret::from_multibase()`, which handles all
/// key types (Ed25519, X25519, P-256) via their multicodec prefix:
///
/// ```ignore
/// use affinidi_tdk::secrets_resolver::secrets::Secret;
///
/// let bundle = client.fetch_did_secrets_bundle("my-context").await?;
/// for entry in &bundle.secrets {
///     let secret = Secret::from_multibase(
///         &entry.private_key_multibase,
///         Some(&entry.key_id),
///     )?;
///     resolver.insert(secret);
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidSecretsBundle {
    /// The DID these secrets belong to.
    pub did: String,
    /// Secret entries (one per verification method).
    pub secrets: Vec<SecretEntry>,
}

/// A single secret entry within a [`DidSecretsBundle`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretEntry {
    /// Verification method ID (e.g. `did:webvh:...#key-0`).
    pub key_id: String,
    /// Key type — determines how to reconstruct the secret.
    pub key_type: KeyType,
    /// Multibase-encoded (Base58BTC) private key with multicodec prefix.
    ///
    /// The multicodec prefix identifies the key type:
    /// - Ed25519 private: `0x1300` — 32-byte Ed25519 seed
    /// - X25519 private: `0x1302` — 32-byte X25519 scalar (from `get_key_secret`)
    ///   or Ed25519 seed for conversion (from provisioning)
    /// - P256 private: `0x1306` — 32-byte P-256 scalar
    ///
    /// Compatible with `Secret::from_multibase()` for direct use in DIDComm.
    pub private_key_multibase: String,
}

/// Convert a [`GetKeySecretResponse`](crate::client::GetKeySecretResponse) into a [`SecretEntry`].
#[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 {
    /// Decode a base64url-no-pad encoded secrets bundle.
    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()))
    }

    /// Encode this bundle as a base64url-no-pad 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))
    }
}

/// Errors when decoding or encoding a [`DidSecretsBundle`].
#[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());
    }
}