use crate::{canonical, decode_canonical};
use candid::Principal;
use dmsg_types::{login::*, *};
pub const DERIVATION_ORIGINS: [&str; 2] = ["https://dmsg.net", "https://panda.fans"];
pub const MIN_TTL_MS: u64 = MINUTE;
pub const MAX_TTL_MS: u64 = 8 * 60 * MINUTE;
pub const MAX_TARGETS: usize = 32;
pub const CALLBACK_PATH: &str = "/auth/callback";
pub const ED25519_SPKI_PREFIX: [u8; 12] = [
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
];
pub fn callback_port(next_url: &str) -> Result<u16> {
let rest = next_url
.strip_prefix("http://127.0.0.1:")
.or_else(|| next_url.strip_prefix("http://[::1]:"))
.ok_or_else(|| invalid("next_url host"))?;
let port = rest
.strip_suffix(CALLBACK_PATH)
.ok_or_else(|| invalid("next_url path"))?;
ensure_valid(
!port.is_empty()
&& port.len() <= 5
&& port.bytes().all(|b| b.is_ascii_digit())
&& !port.starts_with('0'),
"next_url port",
)?;
port.parse().map_err(|_| invalid("next_url port"))
}
pub fn validate_request(request: &LoginRequest) -> Result<()> {
ensure_valid(request.v == LOGIN_VERSION, "v")?;
crate::nonzero(request.state.as_slice())?;
ensure_valid(
request.session_pubkey.len() == 44
&& request.session_pubkey.starts_with(&ED25519_SPKI_PREFIX),
"session_pubkey",
)?;
ensure_valid(
!request.targets.is_empty() && request.targets.len() <= MAX_TARGETS,
"targets",
)?;
for (i, target) in request.targets.iter().enumerate() {
let principal = Principal::from_text(target).map_err(|_| invalid("targets"))?;
ensure_valid(
principal.to_text() == *target && !request.targets[..i].contains(target),
"targets",
)?;
}
ensure_valid(
(MIN_TTL_MS..=MAX_TTL_MS).contains(&request.ttl_ms),
"ttl_ms",
)?;
ensure_valid(
DERIVATION_ORIGINS.contains(&request.derivation_origin.as_str()),
"derivation_origin",
)?;
callback_port(&request.next_url).map(|_| ())
}
pub fn encode_login<T: serde::Serialize>(message: &T) -> Vec<u8> {
canonical(message)
}
pub fn decode_request(bytes: &[u8]) -> Result<LoginRequest> {
let request: LoginRequest = decode_canonical(bytes)?;
validate_request(&request)?;
Ok(request)
}
pub fn decode_callback(bytes: &[u8]) -> Result<LoginCallback> {
let callback: LoginCallback = decode_canonical(bytes)?;
let v = match &callback {
LoginCallback::Result(result) => result.v,
LoginCallback::Error(error) => error.v,
};
ensure(v == LOGIN_VERSION, Error::IntegrityFailed)?;
Ok(callback)
}
#[cfg(test)]
#[path = "login_tests.rs"]
mod tests;