s0_wire 0.1.2

sync0 紧凑二进制线协议、TACK 双层确认帧与 Ed25519 自签名节点名片编解码库
use std::fmt;

use coarsetime::Clock;
use ed25519_dalek::{Signature, Signer as _, SigningKey, Verifier as _, VerifyingKey};
use s0_cdc::{Rd, WireError, Wr};
use s0_const::{
  AttrKey, MAX_BIO_CHARS, MAX_EMAIL_LEN, MAX_NAME_CHARS, MAX_URL_LEN, MIN_PROFILE_WIRE_LEN,
  PEER_ID_LEN, PROFILE_MAGIC, SECRET_KEY_LEN, SIG_LEN, region::Region,
};
use thiserror::Error;

/// 与 `iroh::SecretKey` 二进制完全兼容的 32B Ed25519 节点私钥(无 `tokio` 传递依赖)
#[derive(Debug, Clone)]
pub struct SecretKey(SigningKey);

impl SecretKey {
  #[inline]
  pub fn from_bytes(bytes: &[u8; SECRET_KEY_LEN]) -> Self {
    Self(SigningKey::from_bytes(bytes))
  }

  #[inline]
  pub fn to_bytes(&self) -> [u8; SECRET_KEY_LEN] {
    self.0.to_bytes()
  }

  #[inline]
  pub fn public(&self) -> PublicKey {
    PublicKey(self.0.verifying_key())
  }
}

/// 与 `iroh::PublicKey` (`EndpointId`) 二进制完全兼容的 32B Ed25519 节点公钥
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicKey(VerifyingKey);

impl PublicKey {
  #[inline]
  pub fn as_bytes(&self) -> &[u8; PEER_ID_LEN] {
    self.0.as_bytes()
  }
}

/// 节点自签名名片错误定义
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ProfileError {
  #[error(transparent)]
  Wire(#[from] WireError),
  #[error("用户名超过最大 64 字符限制 (当前 {0} 字符)")]
  NameTooLong(usize),
  #[error("邮箱长度超过 128 字节上限")]
  EmailTooLong,
  #[error("网址长度超过 256 字节上限")]
  UrlTooLong,
  #[error("个性签名超过最大 256 字符限制 (当前 {0} 字符)")]
  BioTooLong(usize),
  #[error("未知或乱序的数字属性键: {0}")]
  BadAttrKey(u8),
  #[error("非法的 iroh Ed25519 公钥")]
  BadPublicKey,
  #[error("Ed25519 签名验证失败")]
  BadSignature,
}

/// 基于 `iroh` Ed25519 公钥自签名的对等节点名片
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedProfile {
  pub pubkey: [u8; PEER_ID_LEN],
  pub ts_sec: u64,
  pub name: String,
  pub email: String,
  pub url: String,
  pub region: Region,
  pub bio: String,
  pub sig: [u8; SIG_LEN],
}

/// 待签名节点资料构建器
#[derive(Debug, Default, Clone)]
pub struct ProfileDraft {
  pub name: String,
  pub email: String,
  pub url: String,
  pub region: Region,
  pub bio: String,
}

impl ProfileDraft {
  pub fn new(name: impl Into<String>, region: Region) -> Self {
    Self {
      name: name.into(),
      email: String::new(),
      url: String::new(),
      region,
      bio: String::new(),
    }
  }

  pub fn with_email(mut self, email: impl Into<String>) -> Self {
    self.email = email.into();
    self
  }

  pub fn with_url(mut self, url: impl Into<String>) -> Self {
    self.url = url.into();
    self
  }

  pub fn with_bio(mut self, bio: impl Into<String>) -> Self {
    self.bio = bio.into();
    self
  }

  fn validate(&self) -> Result<(), ProfileError> {
    let name_chars = self.name.chars().count();
    if name_chars > MAX_NAME_CHARS {
      return Err(ProfileError::NameTooLong(name_chars));
    }
    if self.email.len() > MAX_EMAIL_LEN {
      return Err(ProfileError::EmailTooLong);
    }
    if self.url.len() > MAX_URL_LEN {
      return Err(ProfileError::UrlTooLong);
    }
    let bio_chars = self.bio.chars().count();
    if bio_chars > MAX_BIO_CHARS {
      return Err(ProfileError::BioTooLong(bio_chars));
    }
    Ok(())
  }

  /// 使用节点 `iroh::SecretKey` 对数字常量属性表进行规范化 Ed25519 签名
  pub fn sign(self, sk: &SecretKey) -> Result<SignedProfile, ProfileError> {
    self.validate()?;
    let pubkey = *sk.public().as_bytes();
    let ts_sec = Clock::now_since_epoch().as_secs();
    let mut profile = SignedProfile {
      pubkey,
      ts_sec,
      name: self.name,
      email: self.email,
      url: self.url,
      region: self.region,
      bio: self.bio,
      sig: [0u8; SIG_LEN],
    };
    let mut body = Vec::with_capacity(128);
    profile.encode_body(&mut body)?;
    profile.sig = sk.0.sign(&body).to_bytes();
    Ok(profile)
  }
}

impl SignedProfile {
  /// 编码除 64B 签名外的规范化 TLV 正文(以 32 位 `PROFILE_MAGIC` 魔数做域分隔,按数值 `AttrKey` 严格升序排列)
  fn encode_body(&self, out: &mut Vec<u8>) -> Result<(), ProfileError> {
    let mut w = Wr::new(out);
    w.u32(PROFILE_MAGIC);
    w.bytes(&self.pubkey);
    w.uvar(self.ts_sec);

    let mut cnt = 0u8;
    if !self.name.is_empty() {
      cnt += 1;
    }
    if !self.email.is_empty() {
      cnt += 1;
    }
    if !self.url.is_empty() {
      cnt += 1;
    }
    if self.region != Region::Unknown {
      cnt += 1;
    }
    if !self.bio.is_empty() {
      cnt += 1;
    }
    w.u8(cnt);

    if !self.name.is_empty() {
      w.u8(AttrKey::Name as u8);
      w.slice_u15(self.name.as_bytes())?;
    }
    if !self.email.is_empty() {
      w.u8(AttrKey::Email as u8);
      w.slice_u15(self.email.as_bytes())?;
    }
    if !self.url.is_empty() {
      w.u8(AttrKey::Url as u8);
      w.slice_u15(self.url.as_bytes())?;
    }
    if self.region != Region::Unknown {
      w.u8(AttrKey::Region as u8);
      w.u16(self.region.as_u16());
    }
    if !self.bio.is_empty() {
      w.u8(AttrKey::Bio as u8);
      w.slice_u15(self.bio.as_bytes())?;
    }
    Ok(())
  }

  /// 序列化为紧凑二进制名片帧
  pub fn encode(&self) -> Result<Vec<u8>, ProfileError> {
    let mut out = Vec::with_capacity(160);
    self.encode_body(&mut out)?;
    out.extend_from_slice(&self.sig);
    Ok(out)
  }

  /// 解码并自动执行 Ed25519 公钥验签与字段约束校验
  pub fn decode_and_verify(buf: &[u8]) -> Result<Self, ProfileError> {
    if buf.len() < MIN_PROFILE_WIRE_LEN {
      return Err(ProfileError::Wire(WireError::Truncated));
    }
    let (body, &sig) = buf
      .split_last_chunk::<SIG_LEN>()
      .ok_or(ProfileError::Wire(WireError::Truncated))?;

    let mut r = Rd::new(body);
    if r.u32()? != PROFILE_MAGIC {
      return Err(ProfileError::Wire(WireError::BadMagic));
    }
    let pubkey = r.array::<PEER_ID_LEN>()?;
    let ts_sec = r.uvar()?;
    let attr_cnt = r.u8()? as usize;

    let mut name = String::new();
    let mut email = String::new();
    let mut url = String::new();
    let mut region = Region::Unknown;
    let mut bio = String::new();
    let mut prev_key = 0u8;

    for _ in 0..attr_cnt {
      let raw_k = r.u8()?;
      if raw_k <= prev_key {
        return Err(ProfileError::BadAttrKey(raw_k));
      }
      prev_key = raw_k;
      let key = AttrKey::from_repr(raw_k).ok_or(ProfileError::BadAttrKey(raw_k))?;
      match key {
        AttrKey::Name => {
          let s = r.str_u15()?;
          let chars = s.chars().count();
          if chars > MAX_NAME_CHARS {
            return Err(ProfileError::NameTooLong(chars));
          }
          name = s.to_owned();
        }
        AttrKey::Email => {
          let s = r.str_u15()?;
          if s.len() > MAX_EMAIL_LEN {
            return Err(ProfileError::EmailTooLong);
          }
          email = s.to_owned();
        }
        AttrKey::Url => {
          let s = r.str_u15()?;
          if s.len() > MAX_URL_LEN {
            return Err(ProfileError::UrlTooLong);
          }
          url = s.to_owned();
        }
        AttrKey::Region => {
          region = Region::from_u16(r.u16()?);
        }
        AttrKey::Bio => {
          let s = r.str_u15()?;
          let chars = s.chars().count();
          if chars > MAX_BIO_CHARS {
            return Err(ProfileError::BioTooLong(chars));
          }
          bio = s.to_owned();
        }
      }
    }

    let vk = VerifyingKey::from_bytes(&pubkey).map_err(|_| ProfileError::BadPublicKey)?;
    vk.verify(body, &Signature::from_bytes(&sig))
      .map_err(|_| ProfileError::BadSignature)?;

    Ok(Self {
      pubkey,
      ts_sec,
      name,
      email,
      url,
      region,
      bio,
      sig,
    })
  }
}

impl fmt::Display for SignedProfile {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let disp_name = if self.name.is_empty() {
      "anonymous"
    } else {
      &self.name
    };
    write!(
      f,
      "{} [{}/{}]",
      disp_name,
      self.region.name_zh(),
      self.region.code()
    )?;
    if !self.email.is_empty() {
      write!(f, " <{}>", self.email)?;
    }
    if !self.url.is_empty() {
      write!(f, " ({})", self.url)?;
    }
    if !self.bio.is_empty() {
      write!(f, " \"{}\"", self.bio)?;
    }
    write!(
      f,
      " #{:02x}{:02x}..{:02x}{:02x}",
      self.pubkey[0], self.pubkey[1], self.pubkey[30], self.pubkey[31]
    )
  }
}