signer-core 0.3.2

Signer core package.
Documentation
use crate::{Result, SignerError};
use base64::{prelude::BASE64_URL_SAFE, Engine};
use chrono::TimeDelta;
use serde::{Deserialize, Serialize};

use crate::{signer_user::SignerUser, SignerSigned, SignerUserPublic};

#[derive(Debug, Serialize, Deserialize)]
pub struct SignerJWT {
    pub header: SignerJWTHeader,
    pub claims: SignerJWTClaims,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SignerJWTHeader {
    pub typ: String,
    pub alg: String,
    pub jwk: SignerJWK,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SignerJWTClaims {
    pub aud: String,
    pub jti: String,
    pub iat: i64,
    pub exp: i64,
    pub nbf: i64,
    pub iss: String,
    pub sub: String,
    pub user_public: SignerSigned<SignerUserPublic>,
    pub extra: Option<serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SignerJWK {
    pub kty: String,
    pub alg: String,
    pub crv: String,
    pub x: String,
}

impl SignerJWT {
    pub fn new(header: SignerJWTHeader, claims: SignerJWTClaims) -> Self {
        Self { header, claims }
    }

    pub fn decode_unverify(str: &str) -> Result<Self> {
        let split: Vec<&str> = str.split(".").collect();

        let header_buffer = BASE64_URL_SAFE.decode(split[0].as_bytes())?;
        let claims_buffer = BASE64_URL_SAFE.decode(split[1].as_bytes())?;

        let header: SignerJWTHeader = serde_json::from_str(&String::from_utf8(header_buffer)?)?;
        let claims: SignerJWTClaims = serde_json::from_str(&String::from_utf8(claims_buffer)?)?;

        Ok(SignerJWT::new(header, claims))
    }

    pub fn decode(str: &str) -> Result<Self> {
        let split: Vec<&str> = str.split(".").collect();

        let header_buffer = BASE64_URL_SAFE.decode(split[0].as_bytes())?;
        let claims_buffer = BASE64_URL_SAFE.decode(split[1].as_bytes())?;

        let header: SignerJWTHeader = serde_json::from_str(&String::from_utf8(header_buffer)?)?;
        let claims: SignerJWTClaims = serde_json::from_str(&String::from_utf8(claims_buffer)?)?;

        let base = serde_json::to_string(&[split[0], split[1]].join("."))?;
        let base = BASE64_URL_SAFE.encode(base.as_bytes());
        let sig_str = split[2];

        // verify signature
        let sig: SignerSigned<String> =
            SignerSigned::from_field(&header.jwk.x.clone(), &base, sig_str)?;
        sig.verify_to_value()?;

        // verify claims
        let u = claims.user_public.verify_to_value()?;
        if header.jwk.x != u.pub_key {
            return Err(SignerError::Msg(
                "invalid claims, header key is not equal to claims user public key".to_string(),
            ));
        }
        claims.verify()?;

        Ok(SignerJWT::new(header, claims))
    }

    pub fn encode(&self, u: &SignerUser) -> Result<String> {
        let header = BASE64_URL_SAFE.encode(serde_json::to_string(&self.header)?.as_bytes());
        let claims = BASE64_URL_SAFE.encode(serde_json::to_string(&self.claims)?.as_bytes());

        let base = [header, claims].join(".");
        let sig = SignerSigned::from_value(u, &base)?;

        Ok([base, sig.sig].join("."))
    }
}

impl SignerJWTClaims {
    pub fn default(signer: &SignerUser, aud: String, jti: String) -> Self {
        let now = chrono::Local::now();
        Self {
            aud,
            jti,
            iat: now.timestamp_millis(),
            exp: now
                .checked_add_signed(TimeDelta::hours(72))
                .unwrap()
                .timestamp_millis(),
            nbf: now
                .checked_sub_signed(TimeDelta::minutes(5))
                .unwrap()
                .timestamp_millis(),
            iss: format!(""),
            sub: format!(""),
            extra: None,
            user_public: SignerSigned::from_value(signer, &signer.public.clone())
                .expect("sign user public failed"),
        }
    }

    pub fn with_issuer(mut self, issuer: &str) -> Self {
        self.iss = issuer.to_string();
        self
    }

    pub fn with_expired_duration(mut self, expired_time: chrono::Duration) -> Self {
        self.exp = chrono::DateTime::from_timestamp_millis(self.iat)
            .unwrap()
            .checked_add_signed(expired_time)
            .unwrap()
            .timestamp_millis();
        self
    }

    pub fn with_subject(mut self, subject: &str) -> Self {
        self.sub = subject.to_string();
        self
    }

    pub fn verify(&self) -> Result<()> {
        let now = chrono::Local::now();

        if now.timestamp_millis() < self.nbf {
            return Err(SignerError::Msg(format!(
                "blocked by token not before field, nbf {} but now {}",
                self.nbf,
                now.timestamp_millis()
            )));
        }

        if now.timestamp_millis() > self.exp {
            return Err(SignerError::Msg(format!(
                "blocked by token expired field, exp {} but now {}",
                self.exp,
                now.timestamp_millis()
            )));
        }

        Ok(())
    }
}

impl SignerJWTHeader {
    pub fn default(u: &SignerUser) -> Self {
        Self {
            typ: format!("JWT"),
            alg: format!("Schnorr SHA3-256"),
            jwk: SignerJWK::default(u),
        }
    }
}

impl SignerJWK {
    fn default(u: &SignerUser) -> Self {
        Self {
            kty: format!("OKP"),
            alg: format!("schnorr"),
            crv: format!("sr25519"),
            x: u.public.pub_key.clone(),
        }
    }
}