use ed25519_dalek::{SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::time::SystemTime;
use crate::error::{RegistryError, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EntityId(String);
impl EntityId {
pub fn from_public_key(key: &[u8]) -> Result<Self> {
if key.len() != 32 {
return Err(RegistryError::InvalidKey(format!(
"expected 32-byte Ed25519 public key, got {} bytes",
key.len()
)));
}
let encoded = bs58::encode(&key[..16]).into_string();
Ok(Self(format!("clasp:{}", encoded)))
}
pub fn parse(s: &str) -> Result<Self> {
if !s.starts_with("clasp:") {
return Err(RegistryError::InvalidId(format!(
"entity ID must start with 'clasp:', got: {}",
s
)));
}
let suffix = &s[6..];
bs58::decode(suffix)
.into_vec()
.map_err(|e| RegistryError::InvalidId(format!("invalid base58 in entity ID: {}", e)))?;
Ok(Self(s.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<EntityId> for String {
fn from(id: EntityId) -> Self {
id.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EntityType {
Device,
User,
Service,
Router,
}
impl fmt::Display for EntityType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EntityType::Device => write!(f, "device"),
EntityType::User => write!(f, "user"),
EntityType::Service => write!(f, "service"),
EntityType::Router => write!(f, "router"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum EntityStatus {
#[default]
Active,
Suspended,
Revoked,
}
impl fmt::Display for EntityStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EntityStatus::Active => write!(f, "active"),
EntityStatus::Suspended => write!(f, "suspended"),
EntityStatus::Revoked => write!(f, "revoked"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub id: EntityId,
pub entity_type: EntityType,
pub name: String,
#[serde(with = "hex_bytes")]
pub public_key: Vec<u8>,
pub created_at: SystemTime,
#[serde(default)]
pub metadata: HashMap<String, String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub namespaces: Vec<String>,
#[serde(default)]
pub scopes: Vec<String>,
#[serde(default)]
pub status: EntityStatus,
}
impl Entity {
pub fn is_active(&self) -> bool {
self.status == EntityStatus::Active
}
}
pub struct EntityKeypair {
pub entity_id: EntityId,
pub signing_key: SigningKey,
pub verifying_key: VerifyingKey,
}
impl EntityKeypair {
pub fn generate() -> Result<Self> {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let entity_id = EntityId::from_public_key(verifying_key.as_bytes())?;
Ok(Self {
entity_id,
signing_key,
verifying_key,
})
}
pub fn from_signing_key(signing_key: SigningKey) -> Result<Self> {
let verifying_key = signing_key.verifying_key();
let entity_id = EntityId::from_public_key(verifying_key.as_bytes())?;
Ok(Self {
entity_id,
signing_key,
verifying_key,
})
}
pub fn public_key_bytes(&self) -> &[u8] {
self.verifying_key.as_bytes()
}
pub fn to_entity(&self, entity_type: EntityType, name: String) -> Entity {
Entity {
id: self.entity_id.clone(),
entity_type,
name,
public_key: self.verifying_key.as_bytes().to_vec(),
created_at: SystemTime::now(),
metadata: HashMap::new(),
tags: Vec::new(),
namespaces: Vec::new(),
scopes: Vec::new(),
status: EntityStatus::Active,
}
}
}
impl fmt::Debug for EntityKeypair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EntityKeypair")
.field("entity_id", &self.entity_id)
.field("verifying_key", &"[redacted]")
.finish()
}
}
mod hex_bytes {
use serde::{self, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex_string: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
serializer.serialize_str(&hex_string)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(serde::de::Error::custom))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_id_from_public_key() {
let keypair = EntityKeypair::generate().unwrap();
let id = &keypair.entity_id;
assert!(id.as_str().starts_with("clasp:"));
assert!(id.as_str().len() > 10);
}
#[test]
fn test_entity_id_parse() {
let keypair = EntityKeypair::generate().unwrap();
let id_str = keypair.entity_id.as_str();
let parsed = EntityId::parse(id_str).unwrap();
assert_eq!(parsed, keypair.entity_id);
}
#[test]
fn test_entity_id_parse_invalid() {
assert!(EntityId::parse("invalid").is_err());
assert!(EntityId::parse("clasp:!!!").is_err());
}
#[test]
fn test_keypair_generate() {
let kp1 = EntityKeypair::generate().unwrap();
let kp2 = EntityKeypair::generate().unwrap();
assert_ne!(kp1.entity_id, kp2.entity_id);
}
#[test]
fn test_to_entity() {
let keypair = EntityKeypair::generate().unwrap();
let entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
assert_eq!(entity.id, keypair.entity_id);
assert_eq!(entity.entity_type, EntityType::Device);
assert_eq!(entity.name, "test-device");
assert!(entity.is_active());
}
#[test]
fn test_entity_status() {
let keypair = EntityKeypair::generate().unwrap();
let mut entity = keypair.to_entity(EntityType::User, "test-user".to_string());
assert!(entity.is_active());
entity.status = EntityStatus::Suspended;
assert!(!entity.is_active());
entity.status = EntityStatus::Revoked;
assert!(!entity.is_active());
}
}