use crate::dht::Keypair;
use ring::hkdf::{HKDF_SHA256, Salt};
use std::path::Path;
pub const KEY: &str = "rendezvous.key";
pub const SALT: &str = "pairing.salt";
pub const PUBLIC: &str = "rendezvous.pub";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handoff {
pub public: String,
pub salt: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Pairing {
pub(crate) seed: [u8; 32],
pub(crate) salt: [u8; 32],
}
impl Pairing {
pub(crate) fn keypair(&self) -> Result<Keypair, String> {
Keypair::from_seed(self.seed)
}
pub(crate) fn handoff(&self) -> Result<Handoff, String> {
Ok(Handoff {
public: hex(&self.keypair()?.public()),
salt: hex(&self.salt),
})
}
pub(crate) fn inbox_keypair(&self) -> Result<Keypair, String> {
Keypair::from_seed(self.derive(b"inbox key"))
}
pub(crate) fn presence_salt(&self) -> Vec<u8> {
self.derive(b"presence salt").to_vec()
}
pub(crate) fn inbox_salt(&self) -> Vec<u8> {
self.derive(b"inbox salt").to_vec()
}
pub(crate) fn seal_key(&self) -> [u8; 32] {
self.derive(b"seal key")
}
fn derive(&self, label: &[u8]) -> [u8; 32] {
let mut out = [0u8; 32];
if let Ok(okm) = Salt::new(HKDF_SHA256, b"yog rendezvous")
.extract(&self.salt)
.expand(&[label], HKDF_SHA256)
{
let _ = okm.fill(&mut out);
}
out
}
}
pub(crate) fn read_dir(dir: &Path) -> Result<Option<Pairing>, String> {
let (seed, salt) = (hex_file(&dir.join(KEY)), hex_file(&dir.join(SALT)));
match (seed, salt) {
(None, None) => Ok(None),
(Some(seed), Some(salt)) => Ok(Some(Pairing { seed, salt })),
_ => Err(format!(
"the rendezvous material at {} is half there: {KEY} and {SALT} are minted \
together — run `{}`",
dir.display(),
super::super::material::REMEDY
)),
}
}
pub(crate) fn mint(dir: &Path) -> Result<(), String> {
for name in [KEY, SALT] {
let path = dir.join(name);
if path.is_file() {
continue;
}
let mut bytes = [0u8; 32];
crate::dht::random(&mut bytes)?;
std::fs::write(&path, format!("{}\n", hex(&bytes)))
.map_err(|e| format!("{}: {e}", path.display()))?;
super::super::provision::private(&path, 0o600);
}
publish(dir)
}
fn publish(dir: &Path) -> Result<(), String> {
let path = dir.join(PUBLIC);
if path.is_file() {
return Ok(());
}
let seed = hex_file(&dir.join(KEY))
.ok_or_else(|| format!("{} is not 32 bytes of hex", dir.join(KEY).display()))?;
let public = crate::dht::Keypair::from_seed(seed)?.public();
std::fs::write(&path, format!("{}\n", hex(&public)))
.map_err(|e| format!("{}: {e}", path.display()))?;
super::super::provision::private(&path, 0o644);
Ok(())
}
pub(crate) fn bundle(dir: &Path) -> Result<Vec<String>, String> {
if read_dir(dir)?.is_none() {
return Ok(Vec::new());
}
publish(dir)?;
Ok(vec![PUBLIC.to_owned(), SALT.to_owned()])
}
pub(crate) fn artifacts() -> Vec<String> {
vec![KEY.to_owned(), SALT.to_owned(), PUBLIC.to_owned()]
}
fn hex_file(path: &Path) -> Option<[u8; 32]> {
let text = std::fs::read_to_string(path).ok()?;
let bytes = unhex(text.trim())?;
<[u8; 32]>::try_from(bytes).ok()
}
pub(crate) fn hex(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
})
}
pub(crate) fn unhex(text: &str) -> Option<Vec<u8>> {
if !text.len().is_multiple_of(2) {
return None;
}
text.as_bytes()
.chunks(2)
.map(|pair| {
let pair = std::str::from_utf8(pair).ok()?;
u8::from_str_radix(pair, 16).ok()
})
.collect()
}
#[cfg(test)]
mod tests;