From 08589f01367df4b6ddb2d7cf0ead9c3dbdd1dd53 Mon Sep 17 00:00:00 2001 From: Monster0506 Date: Fri, 24 Jul 2026 20:55:14 -0400 Subject: [PATCH] encrypt flags for binary challenges, so it can be reversed to the nsjail container --- .env.example | 2 ++ docker-compose.yml | 1 + src/lib/database/db.ts | 28 ++++++++++++---------------- src/lib/server/flag-crypto.ts | 30 ++++++++++++++++++++++++++++++ src/routes/admin/+page.server.ts | 5 +++-- src/routes/compete/+page.server.ts | 5 ++--- 6 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 src/lib/server/flag-crypto.ts diff --git a/.env.example b/.env.example index 087f79b..8108943 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,8 @@ MAX_PORT= UPLOADS_DIR= BIN_UPLOADS_DIR= +FLAG_ENCRYPTION_KEY= + # Misc LOG_FILE= PROD= diff --git a/docker-compose.yml b/docker-compose.yml index 07c9e03..ce3458a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -146,6 +146,7 @@ services: UPLOADS_DIR: /app/uploads BIN_UPLOADS_DIR: /app/ctf CHALLENGE_HOST: ${CHALLENGE_HOST:-ctf.hacksu.com} + FLAG_ENCRYPTION_KEY: ${FLAG_ENCRYPTION_KEY} TESTING_READ: "C++ is the Best Language, the very best." SSH_IMAGE_REGISTRY: ${SSH_IMAGE_REGISTRY:-} diff --git a/src/lib/database/db.ts b/src/lib/database/db.ts index b440993..c2d6a9c 100644 --- a/src/lib/database/db.ts +++ b/src/lib/database/db.ts @@ -10,7 +10,8 @@ import { import * as schema from "./auth-schema"; import { env } from "$env/dynamic/private"; // dynamic allows the .env file to be read at runtime -import { randomString } from "$lib/utilities"; +import { randomString, SHA256 } from "$lib/utilities"; +import { decryptFlag } from "$lib/server/flag-crypto"; import { type Stat } from "$lib/mtypes"; const PSQL = postgres({ @@ -649,21 +650,16 @@ export async function CheckFlag(uid: any, cid: any, flag_value: any): Promise<{ return { success: true, message: 'Already Claimed!' }; } } - - // check flag validiity - const row = await db.select() - .from(schema.challenges) - .where( - and( - eq(schema.challenges.id, cid), - eq(schema.challenges.flag, flag_value) - ) - ); - - - // append the flag entry to the user as a claim - const claimed = row.length === 1; + const [challenge] = await db.select({ + flag: schema.challenges.flag, + bin_file: schema.challenges.bin_file, + }).from(schema.challenges).where(eq(schema.challenges.id, cid)).limit(1); + + const claimed = challenge && (challenge.bin_file + ? await decryptFlag(challenge.flag) === flag_value + : challenge.flag === await SHA256(flag_value)); + if (claimed) { const has_appended = await addClaim(uid, cid); console.log(`[*] Appended Claim Status: ${has_appended}`); @@ -1581,7 +1577,7 @@ export async function CreateInstance(uid: any, cid: any) { body: JSON.stringify({ name: challenge_data[0].name, bin: challenge_data[0].bin, - flag_value: challenge_data[0].flag_value, + flag_value: await decryptFlag(challenge_data[0].flag_value), }) }); const instance_data = await res.json(); diff --git a/src/lib/server/flag-crypto.ts b/src/lib/server/flag-crypto.ts new file mode 100644 index 0000000..51a2b5b --- /dev/null +++ b/src/lib/server/flag-crypto.ts @@ -0,0 +1,30 @@ +import { env } from "$env/dynamic/private"; + +const KEY_HEX = process.env.FLAG_ENCRYPTION_KEY ?? env.FLAG_ENCRYPTION_KEY; + +async function importKey(): Promise { + if (!KEY_HEX || KEY_HEX.length !== 64) { + throw new Error("FLAG_ENCRYPTION_KEY must be a 64-char hex string (32 bytes)"); + } + const keyBytes = Uint8Array.from(Buffer.from(KEY_HEX, "hex")); + return crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt", "decrypt"]); +} + +export async function encryptFlag(plaintext: string): Promise { + const key = await importKey(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext)); + const combined = new Uint8Array(iv.length + ciphertext.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(ciphertext), iv.length); + return Buffer.from(combined).toString("base64"); +} + +export async function decryptFlag(encoded: string): Promise { + const key = await importKey(); + const combined = Buffer.from(encoded, "base64"); + const iv = combined.subarray(0, 12); + const ciphertext = combined.subarray(12); + const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext); + return new TextDecoder().decode(plaintext); +} diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts index 989575d..8e4aa64 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/admin/+page.server.ts @@ -20,6 +20,7 @@ import { join, basename } from "path"; // importing interface alias import type { ChallengeForm } from "$lib/database/db"; import { SHA256 } from '$lib/utilities'; +import { encryptFlag } from '$lib/server/flag-crypto'; const uploadDir = process.env.UPLOADS_DIR ?? join(process.cwd(), "uploads"); const binUploadDir = process.env.BIN_UPLOADS_DIR ?? join(process.cwd(), "ctf"); @@ -105,7 +106,7 @@ export const actions = { written_by: formData.written_by, category: formData.category, difficulty: formData.difficulty, - flag: await SHA256(formData.flag), + flag: formData.bin_file ? await encryptFlag(formData.flag) : await SHA256(formData.flag), points, hlinks: attached_files, hints, @@ -151,7 +152,7 @@ export const actions = { if (!flag_value || flag_value.length === 0) { flag_value = await GetFlagHash(formData.id); } else { - flag_value = await SHA256(formData.flag); + flag_value = formData.bin_file ? await encryptFlag(formData.flag) : await SHA256(formData.flag); } try { diff --git a/src/routes/compete/+page.server.ts b/src/routes/compete/+page.server.ts index 6b115da..d40fa39 100644 --- a/src/routes/compete/+page.server.ts +++ b/src/routes/compete/+page.server.ts @@ -2,7 +2,6 @@ import { auth, isAdmin } from '$lib/server/auth'; import { redirect, fail } from '@sveltejs/kit'; import { env } from "$env/dynamic/private"; -import { SHA256 } from '$lib/utilities'; import { GetProgress, GetChallenges, CheckFlag, IsSiteActive, GetCompletions, GetSolversCount, @@ -61,7 +60,7 @@ export const actions = { const form = await request.formData(); let formData = Object.fromEntries(form.entries()) as Record; - const flag_value = await SHA256(formData.flag_value); + const flag_value = formData.flag_value; const cid = formData.cid; const uid = session.user.id; @@ -69,7 +68,7 @@ export const actions = { return { success: false, message: !await IsSiteActive() ? 'No flag submitted' : 'Not accepting flags at this time' }; } - console.log(`[${uid}] Checking Flag (${cid}) -> ${flag_value}`); + console.log(`[${uid}] Checking Flag (${cid})`); try { if (!await IsSiteActive()) {