use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; pub type Result = std::result::Result; #[derive(Debug)] pub struct Error(String); impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl std::error::Error for Error {} #[derive(Debug, Clone, PartialEq, Eq)] pub struct Profile { pub display_name: String, pub bio: String, pub xmr_addr: String, pub ts: i64, pub sig: Vec, } const PREFIX: &[u8] = b"prf "; const NAME_MAX: usize = 64; const BIO_MAX: usize = 512; pub fn validate(display_name: &str, bio: &str, xmr_addr: &str) -> Result<()> { check_fields(display_name, bio, xmr_addr) } pub fn sign( identity_sk: &[u8], display_name: &str, bio: &str, xmr_addr: &str, ts: i64, ) -> Result { check_fields(display_name, bio, xmr_addr)?; let sk_bytes: [u8; 32] = identity_sk .try_into() .map_err(|_| Error("identity secret key must be 32 bytes".into()))?; let sk = SigningKey::from_bytes(&sk_bytes); let sig = sk .sign(&sign_msg(display_name, bio, xmr_addr, ts)) .to_bytes() .to_vec(); Ok(Profile { display_name: display_name.to_string(), bio: bio.to_string(), xmr_addr: xmr_addr.to_string(), ts, sig, }) } pub fn verify(identity_pk: &[u8], prf: &Profile) -> bool { if check_fields(&prf.display_name, &prf.bio, &prf.xmr_addr).is_err() { return false; } if identity_pk.len() != 32 || prf.sig.len() != 64 { return false; } let pk: [u8; 32] = match identity_pk.try_into() { Ok(p) => p, Err(_) => return false, }; let sig: [u8; 64] = match prf.sig.as_slice().try_into() { Ok(s) => s, Err(_) => return false, }; let Ok(vk) = VerifyingKey::from_bytes(&pk) else { return false; }; vk.verify( &sign_msg(&prf.display_name, &prf.bio, &prf.xmr_addr, prf.ts), &Signature::from_bytes(&sig), ) .is_ok() } pub fn encode(prf: &Profile) -> Vec { let mut out = Vec::from(PREFIX); out.extend_from_slice(prf.display_name.as_bytes()); out.push(b'\n'); out.extend_from_slice(prf.bio.as_bytes()); out.push(b'\n'); out.extend_from_slice(prf.xmr_addr.as_bytes()); out.push(b'\n'); out.extend_from_slice(prf.ts.to_string().as_bytes()); out.push(b'\n'); out.extend_from_slice(to_hex(&prf.sig).as_bytes()); out } pub fn decode(pt: &[u8]) -> Option { let rest = pt.strip_prefix(PREFIX)?; let text = std::str::from_utf8(rest).ok()?; let mut parts = text.splitn(5, '\n'); let display_name = parts.next()?.to_string(); let bio = parts.next()?.to_string(); let xmr_addr = parts.next()?.to_string(); let ts = parts.next()?.parse::().ok()?; let sig = from_hex(parts.next()?)?; if check_fields(&display_name, &bio, &xmr_addr).is_err() { return None; } Some(Profile { display_name, bio, xmr_addr, ts, sig, }) } fn check_fields(display_name: &str, bio: &str, xmr_addr: &str) -> Result<()> { if display_name.len() > NAME_MAX { return Err(Error("display_name longer than 64 bytes".into())); } if bio.len() > BIO_MAX { return Err(Error("bio longer than 512 bytes".into())); } if display_name.contains('\n') || bio.contains('\n') || xmr_addr.contains('\n') { return Err(Error("profile fields must not contain newlines".into())); } Ok(()) } fn sign_msg(display_name: &str, bio: &str, xmr_addr: &str, ts: i64) -> Vec { let mut msg = Vec::from(display_name.as_bytes()); msg.push(b'\n'); msg.extend_from_slice(bio.as_bytes()); msg.push(b'\n'); msg.extend_from_slice(xmr_addr.as_bytes()); msg.push(b'\n'); msg.extend_from_slice(ts.to_string().as_bytes()); msg } fn to_hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } fn from_hex(s: &str) -> Option> { if s.is_empty() || !s.len().is_multiple_of(2) { return None; } if !s.bytes().all(|c| c.is_ascii_hexdigit()) { return None; } (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) .collect() }