use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ring::hmac;
use ring::signature::{UnparsedPublicKey, ED25519};
use serde_json::Value;
use crate::remote::client::DeviceKey;
pub const UPGRADE_TOKEN: &str = "rk-link/1";
pub const LINK_PATH: &str = "/v1/link";
pub const SCOPE: &str = "link";
pub const H_LINK_ID: &str = "x-rk-link-id";
pub const H_LINK_KEY: &str = "x-rk-link-key";
pub const HEARTBEAT_SECS: u64 = 20;
pub const DEAD_AFTER_SECS: u64 = 60;
pub const MAX_LINE: usize = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameError {
Malformed,
BadTag,
}
fn message(dir: &str, link_id: &str, payload: &[u8]) -> Vec<u8> {
let mut m = Vec::with_capacity(payload.len() + 48);
m.extend_from_slice(UPGRADE_TOKEN.as_bytes());
m.push(b'\n');
m.extend_from_slice(dir.as_bytes());
m.push(b'\n');
m.extend_from_slice(link_id.as_bytes());
m.push(b'\n');
m.extend_from_slice(payload);
m
}
pub fn split_line(line: &str) -> Option<(&str, &str)> {
let line = line.trim_end_matches(['\r', '\n']);
let (tag, payload) = line.split_once(' ')?;
if tag.is_empty() || payload.is_empty() {
None
} else {
Some((tag, payload))
}
}
pub fn sign_c2s(key: &DeviceKey, link_id: &str, payload: &str) -> String {
URL_SAFE_NO_PAD.encode(key.sign(&message("c2s", link_id, payload.as_bytes())))
}
pub fn verify_c2s(public_key: &[u8], link_id: &str, payload: &str, tag: &str) -> bool {
let Ok(sig) = URL_SAFE_NO_PAD.decode(tag) else {
return false;
};
UnparsedPublicKey::new(&ED25519, public_key)
.verify(&message("c2s", link_id, payload.as_bytes()), &sig)
.is_ok()
}
pub fn mac_s2c(key: &[u8; 32], link_id: &str, payload: &str) -> String {
let k = hmac::Key::new(hmac::HMAC_SHA256, key);
URL_SAFE_NO_PAD.encode(hmac::sign(&k, &message("s2c", link_id, payload.as_bytes())).as_ref())
}
pub fn verify_s2c(key: &[u8; 32], link_id: &str, payload: &str, tag: &str) -> bool {
let Ok(t) = URL_SAFE_NO_PAD.decode(tag) else {
return false;
};
let k = hmac::Key::new(hmac::HMAC_SHA256, key);
hmac::verify(&k, &message("s2c", link_id, payload.as_bytes()), &t).is_ok()
}
pub fn c2s_line(key: &DeviceKey, link_id: &str, frame: &Value) -> String {
let payload = frame.to_string();
format!("{} {}\n", sign_c2s(key, link_id, &payload), payload)
}
pub fn s2c_line(key: &[u8; 32], link_id: &str, frame: &Value) -> String {
let payload = frame.to_string();
format!("{} {}\n", mac_s2c(key, link_id, &payload), payload)
}
pub fn open_c2s(public_key: &[u8], link_id: &str, line: &str) -> Result<Value, FrameError> {
let (tag, payload) = split_line(line).ok_or(FrameError::Malformed)?;
if !verify_c2s(public_key, link_id, payload, tag) {
return Err(FrameError::BadTag);
}
serde_json::from_str(payload).map_err(|_| FrameError::Malformed)
}
pub fn open_s2c(key: &[u8; 32], link_id: &str, line: &str) -> Result<Value, FrameError> {
let (tag, payload) = split_line(line).ok_or(FrameError::Malformed)?;
if !verify_s2c(key, link_id, payload, tag) {
return Err(FrameError::BadTag);
}
serde_json::from_str(payload).map_err(|_| FrameError::Malformed)
}