Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ MAX_PORT=
UPLOADS_DIR=
BIN_UPLOADS_DIR=

FLAG_ENCRYPTION_KEY=

# Misc
LOG_FILE=
PROD=
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}
Expand Down
28 changes: 12 additions & 16 deletions src/lib/database/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions src/lib/server/flag-crypto.ts
Original file line number Diff line number Diff line change
@@ -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<CryptoKey> {
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<string> {
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<string> {
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);
}
5 changes: 3 additions & 2 deletions src/routes/admin/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions src/routes/compete/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -61,15 +60,15 @@ export const actions = {
const form = await request.formData();
let formData = Object.fromEntries(form.entries()) as Record<string, string>;

const flag_value = await SHA256(formData.flag_value);
const flag_value = formData.flag_value;
const cid = formData.cid;
const uid = session.user.id;

if (!flag_value || !cid) {
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()) {
Expand Down