altcha-lib-rs 0.3.3

Community implementation of the Altcha library in Rust for your own server application to create and validate challenges and responses.
Documentation
use actix_web::{body, get, http, post, web, App, HttpResponse, HttpServer, ResponseError};
use altcha_lib_rs::{error, Challenge, ChallengeOptions};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use chrono::Utc;
use serde::{Deserialize, Serialize, Serializer};
use std::fmt::{Display, Formatter};

const SECRET_KEY: &str = "super-secret";

#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all(serialize = "camelCase"))]
struct ErrorResponse {
    error: String,
    #[serde(serialize_with = "status_code_into_u16")]
    status_code: http::StatusCode,
}

fn status_code_into_u16<S>(status_code: &http::StatusCode, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_u16(status_code.as_u16())
}

#[derive(Debug, Clone, Deserialize)]
struct SubmitRequest {
    altcha: String,
}

#[derive(Debug, Clone, Serialize)]
struct VerifiedResponse {
    verified: bool,
}

impl Display for ErrorResponse {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.error)
    }
}

impl ResponseError for ErrorResponse {
    fn status_code(&self) -> http::StatusCode {
        self.status_code
    }
    fn error_response(&self) -> HttpResponse<body::BoxBody> {
        HttpResponse::build(self.status_code()).json(&self)
    }
}

impl From<error::Error> for ErrorResponse {
    fn from(other: error::Error) -> Self {
        match other {
            error::Error::WrongChallengeInput(e) => Self {
                error: format!("Failed to create challenge: {:?}", e),
                status_code: http::StatusCode::INTERNAL_SERVER_ERROR,
            },
            error::Error::VerificationMismatchSignature(e) => Self {
                error: format!("Verification mismatch signature {:?}", e),
                status_code: http::StatusCode::BAD_REQUEST,
            },
            error::Error::VerificationMismatchChallenge(e) => Self {
                error: format!("Verification mismatch challenge {:?}", e),
                status_code: http::StatusCode::BAD_REQUEST,
            },
            error::Error::VerificationFailedExpired(e) => Self {
                error: format!("Verification expired {:?}", e),
                status_code: http::StatusCode::BAD_REQUEST,
            },
            _ => Self {
                error: format!("{:?}", other),
                status_code: http::StatusCode::INTERNAL_SERVER_ERROR,
            },
        }
    }
}

impl From<base64::DecodeError> for ErrorResponse {
    fn from(other: base64::DecodeError) -> Self {
        Self {
            error: format!("base64 decode error {:?}", other),
            status_code: http::StatusCode::BAD_REQUEST,
        }
    }
}

impl From<std::str::Utf8Error> for ErrorResponse {
    fn from(other: std::str::Utf8Error) -> Self {
        Self {
            error: format!("utf8 conversion error {:?}", other),
            status_code: http::StatusCode::BAD_REQUEST,
        }
    }
}

#[get("/altcha")]
async fn get_challenge() -> actix_web::Result<web::Json<Challenge>, ErrorResponse> {
    let res = altcha_lib_rs::create_challenge(ChallengeOptions {
        hmac_key: SECRET_KEY,
        expires: Some(Utc::now() + chrono::TimeDelta::minutes(5)),
        ..Default::default()
    })?;
    Ok(web::Json(res))
}

#[post("/submit")]
async fn verify(
    req: web::Form<SubmitRequest>,
) -> actix_web::Result<web::Json<VerifiedResponse>, ErrorResponse> {
    let decoded_payload = BASE64_STANDARD.decode(&req.altcha)?;
    let string_payload = std::str::from_utf8(decoded_payload.as_slice())?;
    altcha_lib_rs::verify_json_solution(string_payload, SECRET_KEY, true)?;
    Ok(web::Json(VerifiedResponse { verified: true }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(get_challenge).service(verify))
        .bind(("127.0.0.1", 3000))?
        .run()
        .await
}