ordinary-auth 0.6.0

Auth for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use std::time::SystemTime;

use anyhow::bail;
#[cfg(feature = "core")]
use blake2::digest::{FixedOutput, Mac};
#[cfg(feature = "core")]
use bytes::{BufMut, Bytes, BytesMut};
#[cfg(feature = "core")]
use std::time::Duration;

#[cfg(feature = "core")]
use crate::{SIG_LEN, ZEROED_KEY};

use crate::{EXP_LEN, MAC_LEN};
#[cfg(feature = "core")]
use ed25519_dalek::{Signature, VerifyingKey};

/// returns Ok(false) if expired
pub fn check_exp(token: &[u8]) -> anyhow::Result<bool> {
    let exp = if token.len() > 8 {
        let exp_as_bytes: [u8; EXP_LEN] = token[0..EXP_LEN].try_into()?;
        u64::from_be_bytes(exp_as_bytes)
    } else {
        bail!("no exp on token");
    };

    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs();

    Ok(exp >= now)
}

pub fn get_exp(token: &[u8]) -> anyhow::Result<u64> {
    let exp = if token.len() > EXP_LEN {
        let exp_as_bytes: [u8; EXP_LEN] = token[0..EXP_LEN].try_into()?;
        u64::from_be_bytes(exp_as_bytes)
    } else {
        bail!("no exp on token");
    };

    Ok(exp)
}

#[cfg(feature = "core")]
pub fn generate_hmac(claims: &[u8], from_now_secs: u32, key: &[u8; 32]) -> anyhow::Result<Bytes> {
    match SystemTime::now().checked_add(Duration::from_secs(u64::from(from_now_secs))) {
        Some(future_time) => {
            let exp_as_bytes: [u8; EXP_LEN] = future_time
                .duration_since(SystemTime::UNIX_EPOCH)?
                .as_secs()
                .to_be_bytes();

            let hmac = blake2::Blake2sMac256::new_from_slice(key)?
                .chain_update(exp_as_bytes)
                .chain_update(claims)
                .finalize_fixed();

            let mut buf = BytesMut::new();

            buf.put(&exp_as_bytes[..]);
            buf.put(&hmac[..]);
            buf.put(claims);

            Ok(buf.into())
        }
        None => bail!("date is out of range"),
    }
}

pub fn extract_hmac_no_check<'a>(token: &'a [u8]) -> anyhow::Result<&'a [u8]> {
    if token.len() >= EXP_LEN + MAC_LEN {
        let claims: &'a [u8] = &token[EXP_LEN + MAC_LEN..];

        Ok(claims)
    } else {
        bail!("no signature for token")
    }
}

#[cfg(feature = "core")]
pub fn verify_hmac<'a>(token: &'a [u8], key: &[u8; 32]) -> anyhow::Result<&'a [u8]> {
    let (exp, exp_as_bytes) = if token.len() > EXP_LEN {
        let exp_as_bytes: [u8; EXP_LEN] = token[0..EXP_LEN].try_into()?;
        (u64::from_be_bytes(exp_as_bytes), exp_as_bytes)
    } else {
        bail!("no exp on token");
    };

    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs();

    if exp < now {
        bail!("token is expired");
    }

    if token.len() >= EXP_LEN + MAC_LEN {
        let claims: &'a [u8] = &token[EXP_LEN + MAC_LEN..];

        let comp = blake2::Blake2sMac256::new_from_slice(key)?
            .chain_update(exp_as_bytes)
            .chain_update(claims)
            .finalize_fixed();

        if comp.as_slice() == &token[EXP_LEN..EXP_LEN + MAC_LEN] {
            Ok(claims)
        } else {
            bail!("invalid token")
        }
    } else {
        bail!("no token hmac")
    }
}

#[cfg(feature = "core")]
pub fn verify_client_signature(token: &[u8], public_key: &[u8; 32]) -> anyhow::Result<()> {
    if public_key == &ZEROED_KEY {
        bail!("public key cannot be all 0s");
    }

    let client_exp_as_bytes =
        token[token.len() - (EXP_LEN + SIG_LEN)..token.len() - SIG_LEN].try_into()?;
    let client_exp = u64::from_be_bytes(client_exp_as_bytes);

    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs();

    if client_exp < now {
        bail!("client exp exceeded");
    }

    let signature_bytes: [u8; SIG_LEN] = token[token.len() - SIG_LEN..].try_into()?;

    let signature = Signature::from_bytes(&signature_bytes);
    let verifying_key = VerifyingKey::from_bytes(public_key)?;

    verifying_key.verify_strict(&token[..token.len() - SIG_LEN], &signature)?;

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;

    use chacha20poly1305::aead::OsRng;
    use ed25519_dalek::{Signer, SigningKey};

    const HMAC_KEY: [u8; 32] = [0u8; 32];

    #[test]
    fn hmac() -> anyhow::Result<()> {
        let token = generate_hmac(b"test", 60 * 60 * 24, &HMAC_KEY)?;
        let payload = verify_hmac(&token[..], &HMAC_KEY)?;

        assert_eq!(payload, b"test");

        Ok(())
    }

    #[test]
    fn signed() -> anyhow::Result<()> {
        let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
        let verifying_key = signing_key.verifying_key();

        let mut token: BytesMut = generate_hmac(b"test", 60 * 60 * 24, &HMAC_KEY)?.into();

        let exp = SystemTime::now()
            .checked_add(Duration::from_secs(5))
            .expect("time to work")
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs();
        token.put_u64(exp);

        let signature = signing_key.sign(&token[..]);
        token.put(&signature.to_bytes()[..]);

        verify_client_signature(&token, verifying_key.as_bytes())
    }
}