use argon2::{Algorithm, Argon2, Params, Version};
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MeshKdfParams {
pub m_cost_kib: u32,
pub t_cost: u32,
pub p_cost: u32,
}
impl Default for MeshKdfParams {
fn default() -> Self {
Self {
m_cost_kib: 65536,
t_cost: 3,
p_cost: 1,
}
}
}
impl MeshKdfParams {
fn to_argon2_params(self) -> Result<Params, MeshKdfError> {
Params::new(self.m_cost_kib, self.t_cost, self.p_cost, Some(32))
.map_err(|e| MeshKdfError::InvalidParams(format!("{e}")))
}
}
#[derive(Clone)]
pub struct MeshSecret {
discovery_key: [u8; 32],
handshake_key: [u8; 32],
fingerprint: [u8; 8],
}
impl MeshSecret {
pub fn derive(
passphrase: &str,
mesh_id: Option<&str>,
params: MeshKdfParams,
) -> Result<Self, MeshKdfError> {
if passphrase.is_empty() {
return Err(MeshKdfError::EmptyPassphrase);
}
let mut salt = Vec::with_capacity(32);
salt.extend_from_slice(b"pim-mesh-v1");
if let Some(id) = mesh_id {
salt.extend_from_slice(id.as_bytes());
}
let argon2 = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
params.to_argon2_params()?,
);
let mut master = [0u8; 32];
argon2
.hash_password_into(passphrase.as_bytes(), &salt, &mut master)
.map_err(|e| MeshKdfError::Argon2(format!("{e}")))?;
let hk = Hkdf::<Sha256>::new(None, &master);
let mut discovery_key = [0u8; 32];
let mut handshake_key = [0u8; 32];
let mut fingerprint = [0u8; 8];
hk.expand(b"pim-disco-v1", &mut discovery_key)
.map_err(|e| MeshKdfError::Hkdf(format!("{e}")))?;
hk.expand(b"pim-handshake-v1", &mut handshake_key)
.map_err(|e| MeshKdfError::Hkdf(format!("{e}")))?;
hk.expand(b"pim-fingerprint-v1", &mut fingerprint)
.map_err(|e| MeshKdfError::Hkdf(format!("{e}")))?;
Ok(Self {
discovery_key,
handshake_key,
fingerprint,
})
}
pub fn discovery_key(&self) -> &[u8; 32] {
&self.discovery_key
}
pub fn handshake_key(&self) -> &[u8; 32] {
&self.handshake_key
}
pub fn fingerprint(&self) -> &[u8; 8] {
&self.fingerprint
}
pub fn fingerprint_hex(&self) -> String {
let mut out = String::with_capacity(16);
for b in self.fingerprint {
out.push_str(&format!("{b:02x}"));
}
out
}
}
impl std::fmt::Debug for MeshSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MeshSecret")
.field("fingerprint", &self.fingerprint_hex())
.finish_non_exhaustive()
}
}
pub fn compute_rfcomm_hello_tag(handshake_key: &[u8; 32], node_id_hex: &str) -> [u8; 32] {
let mut mac = HmacSha256::new_from_slice(handshake_key).expect("HMAC accepts any key size");
mac.update(node_id_hex.as_bytes());
let result = mac.finalize().into_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(&result);
out
}
#[derive(Debug, thiserror::Error)]
pub enum MeshKdfError {
#[error("mesh passphrase must not be empty")]
EmptyPassphrase,
#[error("invalid argon2 parameters: {0}")]
InvalidParams(String),
#[error("argon2 failure: {0}")]
Argon2(String),
#[error("hkdf failure: {0}")]
Hkdf(String),
}
#[cfg(test)]
mod tests;