prf frames are ed25519-signed, friend-visible only (unknown pubkey is ignored). /profile edits self; F5 and /who show the selected friend's last profile.
157 lines
4.3 KiB
Rust
157 lines
4.3 KiB
Rust
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
#[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<u8>,
|
|
}
|
|
|
|
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<Profile> {
|
|
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<u8> {
|
|
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<Profile> {
|
|
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::<i64>().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<u8> {
|
|
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<Vec<u8>> {
|
|
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()
|
|
}
|