Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 2 additions & 60 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,60 +1,2 @@
[package]
name = "ainigma"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "ainigma"
path = "src/bin/cli.rs"

[dependencies]
# Our async runtime
tokio = { version = "1.44", default-features = false, features = [
"macros",
"rt-multi-thread",
] }

# Crypto
sha3 = "0.10"
hmac = "0.12"
rand = "0.9"

serde = { version = "1.0", default-features = false, features = ["derive"] }
toml = "0.8"

# Cli
itertools = "0.14.0"
clap = { version = "4.5", features = ["derive", "cargo"] }
thiserror = "2"
tracing = { version = "0.1.41" }
tracing-subscriber = { version = "0.3.19", features = [] }
aws-sdk-s3 = { version = "1.82.0", default-features = false, features = [
"rt-tokio",
] }
aws-config = { version = "1.6.1", default-features = false, features = [
"client-hyper",
"rt-tokio",
"rustls",
] }
futures = "0.3.31"
moodle-xml = "0.2.0"
serde_json = "1"
once_cell = { version = "1", default-features = false }
tempfile = { version = "3", default-features = false }
[dependencies.uuid]
version = "1"
features = [
"v7", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
"serde",
]

[dev-dependencies]
insta = { version = "1" }
assert_cmd = "2"
predicates = "3"

[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
[workspace]
members = ["ainigma", "ainigma-backend"]
21 changes: 21 additions & 0 deletions ainigma-backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "ainigma-backend"
version = "0.1.0"
edition = "2024"

[dependencies]
ainigma = { path = "../ainigma" }

# and any other dependencies you need:
hyper = "1.6.0"
axum = "0.8.4"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1.17.0", features = ["v7", "serde"] }
serde = { version = "1.0.211", features = ["derive"] }
serde_json = "1"
tower = "0.5.2"
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-rustls"] }

[dev-dependencies]
reqwest = { version = "0.12.19", features = ["json"] }
tokio = { version = "1", features = ["full"] }
81 changes: 81 additions & 0 deletions ainigma-backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## Configuration

With the current implementation, Ainigma requires the following information.

- **TOML configuration file** — Contains core settings for Ainigma.
- **Task number (identifier)** — The specific task identifier.

Other things they need to take for backend to work

- **User ID** — When using the server backend, this should be provided dynamically; currently it is generated using `Uuid::now_v7()`.
- **Course secret**
- **Output_dir**- In the task folder with name "output"

## Software

Sqlx - Database
Smithy - Generating code for backend

## Serverside structure

```
/srv/ainigma/data/
/courses/
Index file (index for quick course lookup and listing)
/<course_id>/ (or name)
config.toml (defined name for pathing)
/<category>/ (name)
/<task_id>/
entrypoint.sh
code_files...
/output/
/<student_uuid_v7>/
task files for student
resource_files/
```

## Database structure

courses (1) ── (many) categories (1) ── (many) tasks
users (1) ── (many) user_task_progress (many) ── (1) tasks

## workflow

[Client]
|
|-- Request (uuid, task_id, course_id) -->
|
[Server]
|-- Load course config
|-- Check if task exists for student (uuid, task_id, course_id)
| |-- Yes: return existing task
| |-- No:
| |-- Generate flags
| |-- Build task using course config
| |-- Save built task
|-- Return task data -->
|-- Add Correct flag / answer to database
[Client receives task and starts solving]
[Client]
|
|-- Does exercise
|
[Server]
|
|-- Check for correct answer -->
|-- Yes: send correct response and add progress
|-- No: send feedback
|
[Client] receives feedback

## Questions

- Category no identifier and task has String
- Course secret storage?
- Changes only when server down? (configuration checked at start and expected to be correct during runtime)
or updates? (updates to config during server runtime, checked in runtime with functionality locked during update process )

## Feedback

- No support for v7 uuid in postgre only v4
- New build function that takes a uuid, and just takes module and task_id
107 changes: 107 additions & 0 deletions ainigma-backend/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use ainigma::flag_generator::{Algorithm, Flag};
use axum::{Json, Router, routing::post};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Deserialize)]
struct GenerateFlagInput {
identifier: String,
algorithm: Algorithm,
user_id: Uuid,
course_secret: String,
task_id: String,
}

#[derive(Serialize, Deserialize)]
struct GenerateFlagOutput {
flag: String,
}

async fn generate_flag_handler(Json(payload): Json<GenerateFlagInput>) -> Json<GenerateFlagOutput> {
let uuid = payload.user_id;

let flag = Flag::new_user_flag(
payload.identifier, // identifier
&payload.algorithm, // algorithm
&payload.course_secret, // secret
&payload.task_id, // taskid
&uuid, // uuid
)
.flag_string();

Json(GenerateFlagOutput { flag })
}

#[tokio::main]
async fn main() {
let app = Router::new().route("/generate-task", post(generate_flag_handler));

println!("Listening on http://localhost:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn app() -> Router {
Router::new().route("/generate-task", post(generate_flag_handler))
}

#[cfg(test)]
mod tests {
use super::*;
use axum::Json;
use std::net::SocketAddr;
use tokio::task;
use uuid::Uuid;

#[tokio::test]
async fn test_generate_task_handler() {
let user_id = Uuid::now_v7();
let input = GenerateFlagInput {
identifier: "id1".to_string(),
user_id: user_id.clone(),
task_id: "task1".to_string(),
algorithm: Algorithm::HMAC_SHA3_256,
course_secret: "secret".to_string(),
};

let response = generate_flag_handler(Json(input)).await;
let output = response.0;

// Assert output is non-empty and contains the task id
assert!(output.flag.starts_with("id1:"));
println!("Generated flag: {}", output.flag);
}

#[tokio::test]
async fn test_generate_task_integration() {
let app = app();

// Spawn the server on a random port
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
task::spawn(async move {
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
});

// Give the server time to start
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

let client = reqwest::Client::new();
let uuid = Uuid::now_v7();
let res = client
.post("http://localhost:3001/generate-task")
.json(&serde_json::json!({
"identifier": "id2",
"algorithm": Algorithm::HMAC_SHA3_256,
"user_id": uuid,
"course_secret": "course42",
"task_id": "taskA"
}))
.send()
.await
.unwrap();

let body: GenerateFlagOutput = res.json().await.unwrap();
assert!(body.flag.starts_with("id2:"));
println!("Flag: {}", body.flag);
}
}
40 changes: 40 additions & 0 deletions ainigma-backend/migrations/0001_ainigma.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE categories (
id SERIAL PRIMARY KEY,
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
number INTEGER
);

CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
title VARCHAR NOT NULL,
description TEXT,
points INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT now()
);


CREATE TABLE user_task_progress (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
completed_at TIMESTAMPTZ,
score INTEGER,
UNIQUE (user_id, task_id)
);
Loading