onionwire/src/profile.rs

158 lines
4.3 KiB
Rust
Raw Normal View History

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()
}