use std::collections::HashMap;
use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::one_or_many::OneOrMany;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeerNumAlgo {
InceptionKey = 0,
GenesisDoc = 1,
MultipleKeys = 2,
}
impl PeerNumAlgo {
pub fn from_char(c: char) -> Option<Self> {
match c {
'0' => Some(PeerNumAlgo::InceptionKey),
'1' => Some(PeerNumAlgo::GenesisDoc),
'2' => Some(PeerNumAlgo::MultipleKeys),
_ => None,
}
}
pub fn to_char(self) -> char {
match self {
PeerNumAlgo::InceptionKey => '0',
PeerNumAlgo::GenesisDoc => '1',
PeerNumAlgo::MultipleKeys => '2',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerPurpose {
Assertion,
Delegation,
Encryption,
Invocation,
Verification,
Service,
}
impl PeerPurpose {
pub fn from_char(c: char) -> Option<Self> {
match c {
'A' => Some(PeerPurpose::Assertion),
'D' => Some(PeerPurpose::Delegation),
'E' => Some(PeerPurpose::Encryption),
'I' => Some(PeerPurpose::Invocation),
'V' => Some(PeerPurpose::Verification),
'S' => Some(PeerPurpose::Service),
_ => None,
}
}
pub fn to_char(self) -> char {
match self {
PeerPurpose::Assertion => 'A',
PeerPurpose::Delegation => 'D',
PeerPurpose::Encryption => 'E',
PeerPurpose::Invocation => 'I',
PeerPurpose::Verification => 'V',
PeerPurpose::Service => 'S',
}
}
pub fn is_key(&self) -> bool {
!matches!(self, PeerPurpose::Service)
}
}
#[derive(Error, Debug)]
pub enum PeerError {
#[error("Unsupported key type")]
UnsupportedKeyType,
#[error("Unsupported curve: {0}")]
UnsupportedCurve(String),
#[error("Syntax error in service definition: {0}")]
ServiceSyntaxError(String),
#[error("Unsupported numalgo. Only 0 and 2 are supported")]
UnsupportedNumalgo,
#[error("Key parsing error: {0}")]
KeyParsingError(String),
#[error("Encoding error: {0}")]
EncodingError(String),
#[error("Internal error: {0}")]
InternalError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeerKeyPurpose {
Verification,
Encryption,
}
impl PeerKeyPurpose {
pub fn to_char(self) -> char {
match self {
PeerKeyPurpose::Verification => 'V',
PeerKeyPurpose::Encryption => 'E',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeerKeyType {
Ed25519,
Secp256k1,
P256,
}
impl PeerKeyType {
pub fn to_crypto_key_type(self) -> affinidi_crypto::KeyType {
match self {
PeerKeyType::Ed25519 => affinidi_crypto::KeyType::Ed25519,
PeerKeyType::Secp256k1 => affinidi_crypto::KeyType::Secp256k1,
PeerKeyType::P256 => affinidi_crypto::KeyType::P256,
}
}
}
#[derive(Debug, Clone)]
pub struct PeerCreateKey {
pub purpose: PeerKeyPurpose,
pub key_type: Option<PeerKeyType>,
pub public_key_multibase: Option<String>,
}
impl PeerCreateKey {
pub fn new(purpose: PeerKeyPurpose, key_type: PeerKeyType) -> Self {
Self {
purpose,
key_type: Some(key_type),
public_key_multibase: None,
}
}
pub fn from_multibase(purpose: PeerKeyPurpose, multibase: String) -> Self {
Self {
purpose,
key_type: None,
public_key_multibase: Some(multibase),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerCreatedKey {
pub key_multibase: String,
pub curve: String,
pub d: String,
pub x: String,
pub y: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerService {
#[serde(rename = "t")]
pub type_: String,
#[serde(rename = "s")]
pub endpoint: PeerServiceEndpoint,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PeerServiceEndpoint {
Uri(String),
Long(OneOrMany<PeerServiceEndpointLong>),
Short(OneOrMany<PeerServiceEndpointShort>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerServiceEndpointShort {
pub uri: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub a: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub r: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerServiceEndpointLong {
pub uri: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accept: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routing_keys: Vec<String>,
}
impl PeerServiceEndpointShort {
pub fn to_long(&self) -> PeerServiceEndpointLong {
PeerServiceEndpointLong {
uri: self.uri.clone(),
accept: self.a.clone(),
routing_keys: self.r.clone(),
}
}
}
impl PeerServiceEndpointLong {
pub fn to_short(&self) -> PeerServiceEndpointShort {
PeerServiceEndpointShort {
uri: self.uri.clone(),
a: self.accept.clone(),
r: self.routing_keys.clone(),
}
}
}
impl PeerService {
pub fn encode(&self) -> Result<String, PeerError> {
let json = serde_json::to_string(self).map_err(|e| {
PeerError::ServiceSyntaxError(format!("Failed to serialize service: {e}"))
})?;
Ok(format!(
"S{}",
BASE64_URL_SAFE_NO_PAD.encode(json.as_bytes())
))
}
pub fn decode(encoded: &str) -> Result<Self, PeerError> {
let encoded = encoded.strip_prefix('S').unwrap_or(encoded);
let bytes = BASE64_URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|e| PeerError::ServiceSyntaxError(format!("Base64 decode failed: {e}")))?;
serde_json::from_slice(&bytes)
.map_err(|e| PeerError::ServiceSyntaxError(format!("JSON parse failed: {e}")))
}
pub fn to_did_service(
&self,
did: &str,
index: u32,
) -> Result<crate::service::Service, PeerError> {
use std::str::FromStr;
use url::Url;
let id_fragment = if let Some(id) = &self.id {
id.clone()
} else if index == 0 {
"#service".to_string()
} else {
format!("#service-{index}")
};
let id = Url::from_str(&format!("{did}{id_fragment}"))
.map_err(|e| PeerError::ServiceSyntaxError(format!("Invalid service ID: {e}")))?;
let service_endpoint = match &self.endpoint {
PeerServiceEndpoint::Uri(uri) => {
let url = Url::from_str(uri)
.map_err(|e| PeerError::ServiceSyntaxError(format!("Invalid URI: {e}")))?;
crate::service::Endpoint::Url(url)
}
PeerServiceEndpoint::Short(endpoints) => {
let value = match endpoints {
OneOrMany::One(ep) => serde_json::to_value(ep.to_long())
.map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?,
OneOrMany::Many(eps) => {
let long: Vec<_> = eps.iter().map(|e| e.to_long()).collect();
serde_json::to_value(long)
.map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?
}
};
crate::service::Endpoint::Map(value)
}
PeerServiceEndpoint::Long(endpoints) => {
let value = serde_json::to_value(endpoints)
.map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?;
crate::service::Endpoint::Map(value)
}
};
Ok(crate::service::Service {
id: Some(id),
type_: vec!["DIDCommMessaging".to_string()],
service_endpoint,
property_set: HashMap::new(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numalgo_from_char_valid() {
assert_eq!(PeerNumAlgo::from_char('0'), Some(PeerNumAlgo::InceptionKey));
assert_eq!(PeerNumAlgo::from_char('1'), Some(PeerNumAlgo::GenesisDoc));
assert_eq!(PeerNumAlgo::from_char('2'), Some(PeerNumAlgo::MultipleKeys));
}
#[test]
fn numalgo_from_char_invalid() {
assert_eq!(PeerNumAlgo::from_char('3'), None);
assert_eq!(PeerNumAlgo::from_char('x'), None);
}
#[test]
fn numalgo_to_char() {
assert_eq!(PeerNumAlgo::InceptionKey.to_char(), '0');
assert_eq!(PeerNumAlgo::GenesisDoc.to_char(), '1');
assert_eq!(PeerNumAlgo::MultipleKeys.to_char(), '2');
}
#[test]
fn numalgo_roundtrip() {
for c in ['0', '1', '2'] {
let algo = PeerNumAlgo::from_char(c).unwrap();
assert_eq!(algo.to_char(), c);
}
}
#[test]
fn purpose_from_char_valid() {
assert_eq!(PeerPurpose::from_char('A'), Some(PeerPurpose::Assertion));
assert_eq!(PeerPurpose::from_char('D'), Some(PeerPurpose::Delegation));
assert_eq!(PeerPurpose::from_char('E'), Some(PeerPurpose::Encryption));
assert_eq!(PeerPurpose::from_char('I'), Some(PeerPurpose::Invocation));
assert_eq!(PeerPurpose::from_char('V'), Some(PeerPurpose::Verification));
assert_eq!(PeerPurpose::from_char('S'), Some(PeerPurpose::Service));
}
#[test]
fn purpose_from_char_invalid() {
assert_eq!(PeerPurpose::from_char('X'), None);
assert_eq!(PeerPurpose::from_char('a'), None);
}
#[test]
fn purpose_to_char() {
assert_eq!(PeerPurpose::Assertion.to_char(), 'A');
assert_eq!(PeerPurpose::Delegation.to_char(), 'D');
assert_eq!(PeerPurpose::Encryption.to_char(), 'E');
assert_eq!(PeerPurpose::Invocation.to_char(), 'I');
assert_eq!(PeerPurpose::Verification.to_char(), 'V');
assert_eq!(PeerPurpose::Service.to_char(), 'S');
}
#[test]
fn purpose_roundtrip() {
for c in ['A', 'D', 'E', 'I', 'V', 'S'] {
let purpose = PeerPurpose::from_char(c).unwrap();
assert_eq!(purpose.to_char(), c);
}
}
#[test]
fn purpose_is_key() {
assert!(PeerPurpose::Assertion.is_key());
assert!(PeerPurpose::Delegation.is_key());
assert!(PeerPurpose::Encryption.is_key());
assert!(PeerPurpose::Invocation.is_key());
assert!(PeerPurpose::Verification.is_key());
assert!(!PeerPurpose::Service.is_key());
}
#[test]
fn key_purpose_to_char() {
assert_eq!(PeerKeyPurpose::Verification.to_char(), 'V');
assert_eq!(PeerKeyPurpose::Encryption.to_char(), 'E');
}
#[test]
fn key_type_to_crypto_key_type() {
assert_eq!(
PeerKeyType::Ed25519.to_crypto_key_type(),
affinidi_crypto::KeyType::Ed25519
);
assert_eq!(
PeerKeyType::Secp256k1.to_crypto_key_type(),
affinidi_crypto::KeyType::Secp256k1
);
assert_eq!(
PeerKeyType::P256.to_crypto_key_type(),
affinidi_crypto::KeyType::P256
);
}
#[test]
fn peer_create_key_new() {
let k = PeerCreateKey::new(PeerKeyPurpose::Verification, PeerKeyType::Ed25519);
assert_eq!(k.purpose, PeerKeyPurpose::Verification);
assert_eq!(k.key_type, Some(PeerKeyType::Ed25519));
assert!(k.public_key_multibase.is_none());
}
#[test]
fn peer_create_key_from_multibase() {
let k = PeerCreateKey::from_multibase(
PeerKeyPurpose::Encryption,
"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(),
);
assert_eq!(k.purpose, PeerKeyPurpose::Encryption);
assert!(k.key_type.is_none());
assert_eq!(
k.public_key_multibase.as_deref(),
Some("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
);
}
#[test]
fn short_to_long_conversion() {
let short = PeerServiceEndpointShort {
uri: "https://example.com/didcomm".to_string(),
a: vec!["didcomm/v2".to_string()],
r: vec!["did:example:123#key-1".to_string()],
};
let long = short.to_long();
assert_eq!(long.uri, "https://example.com/didcomm");
assert_eq!(long.accept, vec!["didcomm/v2"]);
assert_eq!(long.routing_keys, vec!["did:example:123#key-1"]);
}
#[test]
fn long_to_short_conversion() {
let long = PeerServiceEndpointLong {
uri: "https://example.com/didcomm".to_string(),
accept: vec!["didcomm/v2".to_string()],
routing_keys: vec!["did:example:123#key-1".to_string()],
};
let short = long.to_short();
assert_eq!(short.uri, "https://example.com/didcomm");
assert_eq!(short.a, vec!["didcomm/v2"]);
assert_eq!(short.r, vec!["did:example:123#key-1"]);
}
#[test]
fn short_long_roundtrip() {
let short = PeerServiceEndpointShort {
uri: "https://example.com".to_string(),
a: vec!["a".to_string(), "b".to_string()],
r: vec![],
};
let roundtripped = short.to_long().to_short();
assert_eq!(roundtripped.uri, short.uri);
assert_eq!(roundtripped.a, short.a);
assert_eq!(roundtripped.r, short.r);
}
#[test]
fn service_encode_decode_roundtrip() {
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Uri("https://example.com/didcomm".to_string()),
id: None,
};
let encoded = svc.encode().unwrap();
assert!(encoded.starts_with('S'));
let decoded = PeerService::decode(&encoded).unwrap();
assert_eq!(decoded.type_, "dm");
if let PeerServiceEndpoint::Uri(uri) = &decoded.endpoint {
assert_eq!(uri, "https://example.com/didcomm");
} else {
panic!("expected Uri endpoint");
}
}
#[test]
fn service_decode_without_s_prefix() {
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
id: None,
};
let encoded = svc.encode().unwrap();
let without_prefix = &encoded[1..];
let decoded = PeerService::decode(without_prefix).unwrap();
assert_eq!(decoded.type_, "dm");
}
#[test]
fn service_decode_invalid_base64() {
assert!(PeerService::decode("S!!!invalid!!!").is_err());
}
#[test]
fn service_decode_invalid_json() {
let encoded = format!("S{}", BASE64_URL_SAFE_NO_PAD.encode(b"not json"));
assert!(PeerService::decode(&encoded).is_err());
}
#[test]
fn to_did_service_with_uri_endpoint() {
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Uri("https://example.com/didcomm".to_string()),
id: None,
};
let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#service");
assert_eq!(did_svc.type_, vec!["DIDCommMessaging"]);
}
#[test]
fn to_did_service_with_index() {
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
id: None,
};
let did_svc = svc.to_did_service("did:peer:2abc", 3).unwrap();
assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#service-3");
}
#[test]
fn to_did_service_with_custom_id() {
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
id: Some("#my-svc".to_string()),
};
let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#my-svc");
}
#[test]
fn to_did_service_with_short_endpoint() {
let short = PeerServiceEndpointShort {
uri: "https://example.com/didcomm".to_string(),
a: vec!["didcomm/v2".to_string()],
r: vec![],
};
let svc = PeerService {
type_: "dm".to_string(),
endpoint: PeerServiceEndpoint::Short(OneOrMany::One(short)),
id: None,
};
let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
assert!(matches!(
did_svc.service_endpoint,
crate::service::Endpoint::Map(_)
));
}
}