From e16383e9b9e19890e43c6c22063165aace1188a7 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:09:17 +0000 Subject: [PATCH] Implement algorithm switching This commit implements every algorithm I have played with so far. It also allows for you to switch which algorithm you want to use at runtime. --- README.md | 8 +- example-app/config.toml | 3 +- example-app/server/api/pow/challenge.get.ts | 76 ++++-- example-app/server/api/pow/challenge.post.ts | 14 +- example-app/server/utils/config.ts | 56 ++-- packages/lib/src/index.ts | 65 +++-- packages/lib/src/solver.ts | 111 ++++++-- packages/lib/src/validator.ts | 270 ++++++++++++++++--- packages/widget/src/pow-captcha.ts | 116 ++++++-- packages/widget/src/solver-worker.ts | 58 +++- packages/widget/src/types/worker.ts | 47 ++-- solver/src/algorithms/algorithms.zig | 15 ++ solver/src/algorithms/argon2.zig | 17 ++ solver/src/algorithms/kctf.zig | 169 ++++++++++++ solver/src/algorithms/sha256.zig | 9 + solver/src/hasher.zig | 31 --- solver/src/kctf.zig | 215 --------------- solver/src/solver.zig | 251 +++++++++++++++-- solver/src/utils.zig | 47 ++++ solver/src/validator.zig | 160 ++++++++--- 20 files changed, 1262 insertions(+), 476 deletions(-) create mode 100644 solver/src/algorithms/algorithms.zig create mode 100644 solver/src/algorithms/argon2.zig create mode 100644 solver/src/algorithms/kctf.zig create mode 100644 solver/src/algorithms/sha256.zig delete mode 100644 solver/src/hasher.zig delete mode 100644 solver/src/kctf.zig create mode 100644 solver/src/utils.zig diff --git a/README.md b/README.md index 5fece17..8920e15 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Impost /ˈimˌpōst/ _noun_ a tax or compulsory payment Impost is a PoW anti-spam solution, or for short, a PoW captcha. Instead of -spying on your users and using heavy captchas, Impost uses PoW to impose a cost -on sending requests. To a single user, this is a negligable few seconds, but at -scale, it can be a significant deterrent to spam. +spying on your users and using heavy, bloated captchas, Impost uses PoW to +impose a cost on sending requests. To a single user, this is a negligable few +seconds, but at scale, it can be a significant deterrent to spam. This is the impost monorepo, containing the following packages: @@ -16,4 +16,4 @@ This is the impost monorepo, containing the following packages: It also contains a `solver` package, which is the PoW solver written in Zig, `@impost/lib` is built on top of, an example of how to use the solver in a -nuxt 3 project. +nuxt 3 project. More in-depth documentation will be added in the future. diff --git a/example-app/config.toml b/example-app/config.toml index e3e4da3..7860602 100644 --- a/example-app/config.toml +++ b/example-app/config.toml @@ -1,4 +1,5 @@ -strategy = "kctf" +algorithm = "argon2id" +strategy = "target_number" [leading_zeroes] difficulty = 4 diff --git a/example-app/server/api/pow/challenge.get.ts b/example-app/server/api/pow/challenge.get.ts index cb0f522..4b708c4 100644 --- a/example-app/server/api/pow/challenge.get.ts +++ b/example-app/server/api/pow/challenge.get.ts @@ -1,34 +1,58 @@ import { defineEventHandler } from 'h3' import { config } from '~~/server/utils/config'; -import { generate_challenge } from '@impost/lib/validator'; -import { ChallengeStrategy } from '@impost/lib'; +import { generate_challenge, kCTFChallengeConfig, Argon2ChallengeConfig, SHA256ChallengeConfig } from '@impost/lib/validator'; +import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib'; import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow'; export default defineEventHandler(async () => { let challenge_config; - // switch (config.strategy) { - // case ChallengeStrategy.LeadingZeroes: - // challenge_config = { - // parameters: { expires_at: CHALLENGE_TIMEOUT_MS }, - // strategy: config.strategy, - // difficulty: config.leading_zeroes?.difficulty!, - // }; - // break; - // case ChallengeStrategy.TargetNumber: - // challenge_config = { - // parameters: { expires_at: CHALLENGE_TIMEOUT_MS }, - // strategy: config.strategy, - // max_number: config.target_number.max_number, - // }; - // break; - // } - switch (config.strategy) { - case ChallengeStrategy.kCTF: + switch (config.algorithm) { + case ChallengeAlgorithm.SHA256: + switch (config.strategy) { + case ChallengeStrategy.LeadingZeroes: + challenge_config = { + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.LeadingZeroes, + difficulty: config.leading_zeroes.difficulty, + parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS }, + } as SHA256ChallengeConfig; + break; + case ChallengeStrategy.TargetNumber: + challenge_config = { + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.TargetNumber, + difficulty: config.target_number.max_number, + parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS }, + } as SHA256ChallengeConfig; + break; + } + break; + case ChallengeAlgorithm.Argon2id: + switch (config.strategy) { + case ChallengeStrategy.LeadingZeroes: + challenge_config = { + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.LeadingZeroes, + difficulty: config.leading_zeroes.difficulty, + parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS }, + } as Argon2ChallengeConfig; + break; + case ChallengeStrategy.TargetNumber: + challenge_config = { + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.TargetNumber, + difficulty: config.target_number.max_number, + parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS }, + } as Argon2ChallengeConfig; + break; + } + break; + case ChallengeAlgorithm.kCTF: challenge_config = { - parameters: { expires_at: CHALLENGE_TIMEOUT_MS }, - strategy: config.strategy, + algorithm: ChallengeAlgorithm.kCTF, difficulty: config.kctf.difficulty, - }; + parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS }, + } as kCTFChallengeConfig; break; } @@ -40,10 +64,10 @@ export default defineEventHandler(async () => { }); } - outstandingChallenges.set(challenge.challenge, { + outstandingChallenges.set(challenge.salt, { challenge, timeout: setTimeout(() => { - console.log("Challenge timed out:", challenge.challenge); - outstandingChallenges.delete(challenge.challenge); + console.log("Challenge timed out:", challenge.salt); + outstandingChallenges.delete(challenge.salt); }, CHALLENGE_TIMEOUT_MS) }); diff --git a/example-app/server/api/pow/challenge.post.ts b/example-app/server/api/pow/challenge.post.ts index 58d39ae..02efaee 100644 --- a/example-app/server/api/pow/challenge.post.ts +++ b/example-app/server/api/pow/challenge.post.ts @@ -4,13 +4,13 @@ import * as z from 'zod'; import { outstandingChallenges } from '~~/server/utils/pow'; const challengeSchema = z.object({ - challenge: z.string().startsWith("s."), - solution: z.string().startsWith("s.") + salt: z.string(), + // either a string if the algorithm is kCTF, or a number if the algorithm is Argon2id or SHA256 + solution: z.string().or(z.number()), }) // post handler that takes in the challenge, and the nonce export default defineEventHandler(async (event) => { - console.log(await readBody(event)); const body = await readValidatedBody(event, challengeSchema.safeParse); if (!body.success) { @@ -20,9 +20,9 @@ export default defineEventHandler(async (event) => { }) } - let { challenge, solution } = body.data; + let { salt, solution } = body.data; - const outstanding_challenge = outstandingChallenges.get(challenge); + const outstanding_challenge = outstandingChallenges.get(salt); if (outstanding_challenge === undefined) { throw createError({ statusCode: 400, @@ -37,8 +37,8 @@ export default defineEventHandler(async (event) => { if (challenge_valid) { // clear the challenge - clearTimeout(outstandingChallenges.get(challenge)!.timeout); - outstandingChallenges.delete(challenge); + clearTimeout(outstandingChallenges.get(salt)!.timeout); + outstandingChallenges.delete(salt); return { message: 'Challenge solved' diff --git a/example-app/server/utils/config.ts b/example-app/server/utils/config.ts index e2ad38a..0a922c3 100644 --- a/example-app/server/utils/config.ts +++ b/example-app/server/utils/config.ts @@ -1,34 +1,52 @@ import { readFileSync } from 'fs'; import { load } from 'js-toml'; import z from 'zod'; -import { ChallengeStrategy } from "@impost/lib"; +import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib"; -// const LeadingZeroesSchema = z.object({ -// strategy: z.literal(ChallengeStrategy.LeadingZeroes), -// leading_zeroes: z.object({ -// difficulty: z.number().int().min(1).max(64), -// }), -// }); +const SHA256Schema = z.discriminatedUnion("strategy", [ + z.object({ + algorithm: z.literal(ChallengeAlgorithm.SHA256), + strategy: z.literal(ChallengeStrategy.LeadingZeroes), + leading_zeroes: z.object({ + difficulty: z.number().int().min(1).max(64), + }), + }), + z.object({ + algorithm: z.literal(ChallengeAlgorithm.SHA256), + strategy: z.literal(ChallengeStrategy.TargetNumber), + target_number: z.object({ + max_number: z.number().int().min(1).max(100_000), + }), + }), +]); -// const TargetNumberSchema = z.object({ -// strategy: z.literal(ChallengeStrategy.TargetNumber), -// target_number: z.object({ -// max_number: z.number().int().min(1).max(100_000), -// }), -// }); +const Argon2idSchema = z.discriminatedUnion("strategy", [ + z.object({ + algorithm: z.literal(ChallengeAlgorithm.Argon2id), + strategy: z.literal(ChallengeStrategy.LeadingZeroes), + leading_zeroes: z.object({ + difficulty: z.number().int().min(1).max(64), + }), + }), + z.object({ + algorithm: z.literal(ChallengeAlgorithm.Argon2id), + strategy: z.literal(ChallengeStrategy.TargetNumber), + target_number: z.object({ + max_number: z.number().int().min(1).max(100_000), + }), + }), +]); -const kCTFSchema = z.object({ - strategy: z.literal(ChallengeStrategy.kCTF), +const KCTFSchema = z.object({ + algorithm: z.literal(ChallengeAlgorithm.kCTF), kctf: z.object({ difficulty: z.number().int().min(1), }), }); -export type Config = z.infer; +export const Config = z.union([SHA256Schema, Argon2idSchema, KCTFSchema]); -export const Config = z.discriminatedUnion('strategy', [ - kCTFSchema, -]); +export type Config = z.infer; export let config: Config; diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 9cfa23b..5f82633 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -1,44 +1,69 @@ import { UUID } from "uuidv7"; export enum ChallengeAlgorithm { + SHA256 = "sha256", Argon2id = "argon2id", + kCTF = "kctf", +} + +export function algorithmToInt(algorithm: ChallengeAlgorithm): number { + switch (algorithm) { + case ChallengeAlgorithm.SHA256: + return 0; + case ChallengeAlgorithm.Argon2id: + return 1; + case ChallengeAlgorithm.kCTF: + return 2; + } } export enum ChallengeStrategy { - kCTF = "kctf", + Null = "null", LeadingZeroes = "leading_zeroes", TargetNumber = "target_number", } +export function strategyToInt(strategy: ChallengeStrategy): number { + switch (strategy) { + case ChallengeStrategy.Null: + return 0; + case ChallengeStrategy.LeadingZeroes: + return 1; + case ChallengeStrategy.TargetNumber: + return 2; + } +} + // In this case, the client will repeatedly hash a number with has until it // finds a hash thaat starts with *difficulty* leading zeroes -// export interface ChallengeLeadingZeroes { -// algorithm: ChallengeAlgorithm; -// strategy: ChallengeStrategy.LeadingZeroes; -// salt: string; // random string -// difficulty: number; -// } +export interface ChallengeLeadingZeroes { + algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.LeadingZeroes; + salt: string; // random string + difficulty: number; +} -// // In this case, the server generates a random number, and the client will hash -// // the salt (a random string) + a random number until it finds a hash that is equal to challenge -// export interface ChallengeTargetNumber { -// algorithm: ChallengeAlgorithm; -// strategy: ChallengeStrategy.TargetNumber; -// salt: string; // random string -// target: string; // hash of salt + random number -// } +// In this case, the server generates a random number, and the client will hash +// the salt (a random string) + a random number until it finds a hash that is equal to challenge +export interface ChallengeTargetNumber { + algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.TargetNumber; + salt: string; // random string + target: string; // hash of salt + random number +} export interface InnerChallengekCTF { - strategy: ChallengeStrategy.kCTF; + algorithm: ChallengeAlgorithm.kCTF; salt: UUID; // UUIDv7 difficulty: number; } export interface ChallengekCTF { - strategy: ChallengeStrategy.kCTF; - challenge: string; + algorithm: ChallengeAlgorithm.kCTF; + salt: string; + difficulty: number; } -export type InnerChallenge = InnerChallengekCTF; +export type InnerChallenge = InnerChallengekCTF | ChallengeLeadingZeroes | ChallengeTargetNumber; -export type Challenge = ChallengekCTF; \ No newline at end of file +export type Challenge = ChallengekCTF | ChallengeLeadingZeroes | ChallengeTargetNumber; \ No newline at end of file diff --git a/packages/lib/src/solver.ts b/packages/lib/src/solver.ts index 0ba4144..2cd4b1a 100644 --- a/packages/lib/src/solver.ts +++ b/packages/lib/src/solver.ts @@ -1,12 +1,10 @@ +import { ChallengeAlgorithm, ChallengeStrategy, algorithmToInt, strategyToInt } from "./index"; import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline'; type WasmExports = Record & { "malloc": (byte_count: number) => number | null; "free": (ptr: number | null, byte_count: number) => void; - // "solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number; - // "solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: - // number) => number; - "solve": (value_ptr: number, value_len: number) => number, + "solve": (algorithm: number, strategy: number, salt_ptr: number, salt_len: number, difficulty: number, target_ptr: number, target_len: number) => number, "memory": WebAssembly.Memory; } @@ -32,35 +30,100 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P }) as unknown as SolverModule; } -export function solve(solver: SolverModule, challenge: string): string { - console.log(challenge); +type Argon2LeadingZeroesParams = { + name: ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.LeadingZeroes; + salt: string; + difficulty: number; +}; + +type Argon2TargetNumberParams = { + name: ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.TargetNumber; + salt: string; + target: string; +}; + +type Argon2Params = Argon2LeadingZeroesParams | Argon2TargetNumberParams; + +type SHA256LeadingZeroesParams = { + name: ChallengeAlgorithm.SHA256; + strategy: ChallengeStrategy.LeadingZeroes; + salt: string; + difficulty: number; +}; + +type SHA256TargetNumberParams = { + name: ChallengeAlgorithm.SHA256; + strategy: ChallengeStrategy.TargetNumber; + salt: string; + target: string; +}; + +type SHA256Params = SHA256LeadingZeroesParams | SHA256TargetNumberParams; + +type KCTFParams = { + name: ChallengeAlgorithm.kCTF; + strategy: ChallengeStrategy.Null; + salt: string; + difficulty: number; +}; + +export type SolveParams = Argon2Params | SHA256Params | KCTFParams; + +export function solve(solver: SolverModule, algorithm: SolveParams): string | number { + if (algorithm.name === ChallengeAlgorithm.kCTF) { + algorithm.salt = algorithm.salt.split("?")[0]; + } const encoder = new TextEncoder(); - const challenge_buf = encoder.encode(challenge); + let salt_buf = encoder.encode(algorithm.salt); - const challenge_ptr = solver.exports.malloc(challenge_buf.length); - if (challenge_ptr === 0 || challenge_ptr === null) { + let salt_ptr = solver.exports.malloc(salt_buf.length); + if (salt_ptr === 0 || salt_ptr === null) { throw new Error("Failed to allocate memory for challenge string"); } - const memory = new Uint8Array(solver.exports.memory.buffer); - memory.set(challenge_buf, challenge_ptr); + let memory = new Uint8Array(solver.exports.memory.buffer); + memory.set(salt_buf, salt_ptr); - const ret = solver.exports.solve(challenge_ptr, challenge_buf.length); + let ret: string | number; + switch (algorithm.name) { + case ChallengeAlgorithm.SHA256: + case ChallengeAlgorithm.Argon2id: + switch (algorithm.strategy) { + case ChallengeStrategy.LeadingZeroes: { + ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.LeadingZeroes), salt_ptr, salt_buf.length, algorithm.difficulty, 0, 0); + break; + } + case ChallengeStrategy.TargetNumber: { + const target_buf = encoder.encode(algorithm.target); + const target_ptr = solver.exports.malloc(target_buf.length); + if (target_ptr === 0 || target_ptr === null) { + throw new Error("Failed to allocate memory for target string"); + } - console.log("RET", ret); + memory = new Uint8Array(solver.exports.memory.buffer); + memory.set(target_buf, target_ptr); - if (ret <= 0) { - throw new Error("Failed to solve challenge"); + ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.TargetNumber), salt_ptr, salt_buf.length, 0, target_ptr, target_buf.length); + break; + } + } + break; + case ChallengeAlgorithm.kCTF: + const solution_ptr = solver.exports.solve(algorithmToInt(ChallengeAlgorithm.kCTF), strategyToInt(ChallengeStrategy.Null), salt_ptr, salt_buf.length, algorithm.difficulty, 0, 0); + + if (solution_ptr <= 0) { + throw new Error("Failed to solve challenge"); + } + + const length = new DataView(solver.exports.memory.buffer, solution_ptr, 2).getUint16(0, true); + ret = new TextDecoder().decode(solver.exports.memory.buffer.slice(solution_ptr + 2, solution_ptr + 2 + length)); + + solver.exports.free(solution_ptr, 2 + length); + break; } - const length = new DataView(solver.exports.memory.buffer, ret, 2).getUint16(0, true); - const solution = new TextDecoder().decode(solver.exports.memory.buffer.slice(ret + 2, ret + 2 + length)); - - console.log("SOLUTION", solution); - console.log("LENGTH", length); - - solver.exports.free(ret, 2 + length); - - return solution; + return ret; } diff --git a/packages/lib/src/validator.ts b/packages/lib/src/validator.ts index 3498735..215a838 100644 --- a/packages/lib/src/validator.ts +++ b/packages/lib/src/validator.ts @@ -1,11 +1,12 @@ -import { ChallengeStrategy, type Challenge, type InnerChallenge } from '.'; +import { ChallengeStrategy, type Challenge, type InnerChallenge, ChallengeAlgorithm, algorithmToInt, strategyToInt } from '.'; import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline'; import { uuidv7obj } from 'uuidv7'; type WasmExports = Record & { "malloc": (byte_count: number) => number | null; "free": (ptr: number | null, byte_count: number) => void; - "validate": (challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number) => boolean; + "validate": (algorithm: number, strategy: number, challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number, nonce: number, difficulty: number) => boolean; + "hash": (challenge_ptr: number, challebge_len: number, nonce_ptr: number, nonce_len: number, algorithm: number) => bigint; "memory": WebAssembly.Memory; } @@ -22,39 +23,91 @@ function array_to_base64(buffer: ArrayBufferLike): string { return btoa(binary); } -export interface kCTFChallengeConfig { - parameters: Object; - strategy: ChallengeStrategy.kCTF; +export interface SHA256ChallengeConfig { + algorithm: ChallengeAlgorithm.SHA256; + strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber; difficulty: number; + parameters: Object; } -export type ChallengeConfig = kCTFChallengeConfig; +export interface Argon2ChallengeConfig { + algorithm: ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber; + difficulty: number; + parameters: Object; +} -const VERSION = "s"; +export interface kCTFChallengeConfig { + algorithm: ChallengeAlgorithm.kCTF; + difficulty: number; + parameters: Object; +} -async function encode_challenge(challenge: InnerChallenge, parameters: Object = {}): Promise { - if (challenge.strategy !== ChallengeStrategy.kCTF) { - throw new Error("Unsupported challenge strategy"); +export type ChallengeConfig = kCTFChallengeConfig | SHA256ChallengeConfig | Argon2ChallengeConfig; + +async function encode_challenge(inner_challenge: InnerChallenge, parameters: Object = {}): Promise { + let challenge: Challenge = {} as Challenge; + switch (inner_challenge.algorithm) { + case ChallengeAlgorithm.SHA256: { + challenge.algorithm = ChallengeAlgorithm.SHA256; + challenge.salt = inner_challenge.salt; + switch (inner_challenge.strategy) { + case ChallengeStrategy.LeadingZeroes: { + // @ts-ignore + challenge.strategy = ChallengeStrategy.LeadingZeroes; + // @ts-ignore + challenge.difficulty = inner_challenge.difficulty; + break; + } + case ChallengeStrategy.TargetNumber: { + // @ts-ignore + challenge.strategy = ChallengeStrategy.TargetNumber; + // @ts-ignore + challenge.target = inner_challenge.target; + break; + } + } + break; + } + case ChallengeAlgorithm.Argon2id: { + challenge.algorithm = ChallengeAlgorithm.Argon2id; + challenge.salt = inner_challenge.salt; + switch (inner_challenge.strategy) { + case ChallengeStrategy.LeadingZeroes: { + // @ts-ignore + challenge.strategy = ChallengeStrategy.LeadingZeroes; + // @ts-ignore + challenge.difficulty = inner_challenge.difficulty; + break; + } + case ChallengeStrategy.TargetNumber: { + // @ts-ignore + challenge.strategy = ChallengeStrategy.TargetNumber; + // @ts-ignore + challenge.target = inner_challenge.target; + break; + } + } + break; + } + case ChallengeAlgorithm.kCTF: { + // @ts-ignore + challenge.difficulty = inner_challenge.difficulty; + challenge.algorithm = ChallengeAlgorithm.kCTF; + challenge.salt = array_to_base64(inner_challenge.salt.bytes.buffer); + break; + } } - if (challenge.difficulty < 1) { - throw new Error("Difficulty must be at least 1"); - } - - const difficulty_buf = new Uint8Array(4); - const view = new DataView(difficulty_buf.buffer); - view.setUint32(0, challenge.difficulty, false); - - const difficulty_str = array_to_base64(difficulty_buf.buffer); - const salt_str = array_to_base64(challenge.salt.bytes.buffer); - // the parameters str is expected to be sliced out of the challenge via the widget before it sends it to the wasm solver. let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&"); if (parameters_str.length > 0) { parameters_str = "?" + parameters_str; } - return `${VERSION}.${difficulty_str}.${salt_str}${parameters_str}`; + challenge.salt = challenge.salt + parameters_str; + + return challenge; } export async function generate_challenge(config: ChallengeConfig): Promise { @@ -62,43 +115,180 @@ export async function generate_challenge(config: ChallengeConfig): Promise console.log(new TextDecoder().decode(new Uint8Array(validator.exports.memory.buffer, str_ptr, str_len))), + } + })).instance as unknown as ValidatorModule; + + const encoder = new TextEncoder(); + + var inner_challenge: InnerChallenge = { + algorithm: config.algorithm, + } as InnerChallenge; + let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}`; + let parameters_str: string; + switch (config.algorithm) { + case ChallengeAlgorithm.SHA256: + case ChallengeAlgorithm.Argon2id: + switch (config.strategy) { + case ChallengeStrategy.LeadingZeroes: + if (config.difficulty < 1 || config.difficulty > 64) { + throw new Error("Invalid difficulty for leading zeroes strategy"); + } + + // @ts-ignore + inner_challenge.strategy = ChallengeStrategy.LeadingZeroes; + // @ts-ignore + inner_challenge.difficulty = config.difficulty; + parameters_str = Object.entries(config.parameters).map(([key, value]) => `${key}=${value}`).join("&"); + if (parameters_str.length > 0) { + parameters_str = "?" + parameters_str; + } + inner_challenge.salt = salt + parameters_str; + config.parameters = {}; + break; + case ChallengeStrategy.TargetNumber: + if (config.difficulty < 1) { + throw new Error("Difficulty must be at least 1"); + } + + // @ts-ignore + inner_challenge.strategy = ChallengeStrategy.TargetNumber; + parameters_str = Object.entries(config.parameters).map(([key, value]) => `${key}=${value}`).join("&"); + if (parameters_str.length > 0) { + parameters_str = "?" + parameters_str; + } + inner_challenge.salt = salt + parameters_str; + config.parameters = {}; + + const random_number = Math.floor(Math.random() * config.difficulty).toString(); + console.log("RANDOM NUMBER", random_number); + + const challenge_buf = encoder.encode(inner_challenge.salt + random_number); + const challenge_ptr = validator.exports.malloc(challenge_buf.length); + if (challenge_ptr === 0 || challenge_ptr === null) { + console.error("Failed to allocate memory for challenge string"); + return null; + } + + const memory = new Uint8Array(validator.exports.memory.buffer); + memory.set(challenge_buf, challenge_ptr); + + const challenge_len = inner_challenge.salt.length; + const nonce_ptr = challenge_ptr + challenge_len; + const nonce_len = challenge_buf.length - challenge_len; + + const target = validator.exports.hash(challenge_ptr, challenge_len, nonce_ptr, nonce_len, algorithmToInt(inner_challenge.algorithm)); + + const target_len = Number((target >> 32n) & 0xFFFFFFFFn); + const target_ptr = Number(target & 0xFFFFFFFFn); + + const target_buf = new Uint8Array(validator.exports.memory.buffer, target_ptr, target_len); + // @ts-ignore + inner_challenge.target = new TextDecoder().decode(target_buf); + // @ts-ignore + console.log("TARGET", inner_challenge.target); + + validator.exports.free(challenge_ptr, challenge_len + random_number.length); + validator.exports.free(target_ptr, target_len); + break; + } + break; + case ChallengeAlgorithm.kCTF: + if (config.difficulty < 1) { + throw new Error("Difficulty must be at least 1"); + } + + inner_challenge.salt = uuidv7obj(); + // @ts-ignore + inner_challenge.difficulty = config.difficulty; + break; } - return { - strategy: ChallengeStrategy.kCTF, - challenge: await encode_challenge(challenge), - }; + return await encode_challenge(inner_challenge, config.parameters); } -export async function validate_challenge(challenge: Challenge, challenge_solution: string): Promise { +export async function validate_challenge(challenge: Challenge, challenge_solution: string | number): Promise { const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl), { env: { __log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(validator.exports.memory.buffer, str_ptr, str_len))), } })).instance as unknown as ValidatorModule const encoder = new TextEncoder(); - const challenge_buf = encoder.encode(challenge.challenge); - const solution_buf = encoder.encode(challenge_solution); + if (challenge.algorithm === ChallengeAlgorithm.kCTF) { + challenge.salt = challenge.salt.split("?")[0]; + } + + const challenge_buf = encoder.encode(challenge.salt); const challenge_ptr = validator.exports.malloc(challenge_buf.length); - const solution_ptr = validator.exports.malloc(solution_buf.length); - if (challenge_ptr === 0 || challenge_ptr === null || solution_ptr === 0 || solution_ptr === null) { + if (challenge_ptr === 0 || challenge_ptr === null) { console.error("Failed to allocate memory for challenge string"); return false; } const memory = new Uint8Array(validator.exports.memory.buffer); memory.set(challenge_buf, challenge_ptr); - memory.set(solution_buf, solution_ptr); - const is_valid = validator.exports.validate(challenge_ptr, challenge.challenge.length, solution_ptr, challenge_solution.length); - validator.exports.free(challenge_ptr, challenge.challenge.length); - validator.exports.free(solution_ptr, challenge_solution.length); + switch (challenge.algorithm) { + case ChallengeAlgorithm.SHA256: + if (typeof challenge_solution === "string") { + throw new Error("Argon2id challenges do not support a solution as a number"); + } - return is_valid; + switch (challenge.strategy) { + case ChallengeStrategy.LeadingZeroes: + return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, 0, 0, challenge_solution, challenge.difficulty); + case ChallengeStrategy.TargetNumber: + const solution_buf = encoder.encode(challenge.target); + const solution_ptr = validator.exports.malloc(solution_buf.length); + if (solution_ptr === 0 || solution_ptr === null) { + console.error("Failed to allocate memory for challenge string"); + return false; + } + + const memory = new Uint8Array(validator.exports.memory.buffer); + memory.set(solution_buf, solution_ptr); + + return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, solution_ptr, solution_buf.length, challenge_solution, 0); + } + case ChallengeAlgorithm.Argon2id: + if (typeof challenge_solution === "string") { + throw new Error("Argon2id challenges do not support a solution as a number"); + } + + switch (challenge.strategy) { + case ChallengeStrategy.LeadingZeroes: + return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, 0, 0, challenge_solution, challenge.difficulty); + case ChallengeStrategy.TargetNumber: + const solution_buf = encoder.encode(challenge.target); + const solution_ptr = validator.exports.malloc(solution_buf.length); + if (solution_ptr === 0 || solution_ptr === null) { + console.error("Failed to allocate memory for challenge string"); + return false; + } + + const memory = new Uint8Array(validator.exports.memory.buffer); + memory.set(solution_buf, solution_ptr); + + return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, solution_ptr, solution_buf.length, challenge_solution, 0); + } + case ChallengeAlgorithm.kCTF: + if (typeof challenge_solution === "number") { + throw new Error("KCTF challenges do not support a solution as a number"); + } + + const solution_buf = encoder.encode(challenge_solution); + const solution_ptr = validator.exports.malloc(solution_buf.length); + if (solution_ptr === 0 || solution_ptr === null) { + console.error("Failed to allocate memory for challenge string"); + return false; + } + + const memory = new Uint8Array(validator.exports.memory.buffer); + memory.set(solution_buf, solution_ptr); + return validator.exports.validate(algorithmToInt(challenge.algorithm), 0, challenge_ptr, challenge_buf.length, solution_ptr, solution_buf.length, 0, challenge.difficulty); + } } \ No newline at end of file diff --git a/packages/widget/src/pow-captcha.ts b/packages/widget/src/pow-captcha.ts index 6f9c5d9..0bcf6d0 100644 --- a/packages/widget/src/pow-captcha.ts +++ b/packages/widget/src/pow-captcha.ts @@ -1,7 +1,7 @@ import { LitElement, html, css, isServer, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker'; -import { type Challenge, ChallengeStrategy } from '@impost/lib'; +import { type Challenge, ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib'; import { get_wasm_module } from '@impost/lib/solver'; import ChallengeWorker from './solver-worker?worker&inline'; @@ -172,7 +172,7 @@ export class PowCaptcha extends LitElement { async initWorkers() { this.solverWorkers = []; - const num_workers = 1; + const num_workers = navigator.hardwareConcurrency || 4; for (let i = 0; i < num_workers; i++) { this.solverWorkers.push(new ChallengeWorker()); } @@ -275,11 +275,52 @@ export class PowCaptcha extends LitElement { // } as WorkerRequest); // break; // } - switch (request.strategy) { - case ChallengeStrategy.kCTF: + switch (request.algorithm) { + case ChallengeAlgorithm.SHA256: + switch (request.strategy) { + case ChallengeStrategy.LeadingZeroes: + worker.postMessage({ + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.LeadingZeroes, + salt: request.salt, + difficulty: request.difficulty, + } as WorkerRequest); + break; + case ChallengeStrategy.TargetNumber: + worker.postMessage({ + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.TargetNumber, + target: request.target, + salt: request.salt, + } as WorkerRequest); + break; + } + break; + case ChallengeAlgorithm.Argon2id: + switch (request.strategy) { + case ChallengeStrategy.LeadingZeroes: + worker.postMessage({ + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.LeadingZeroes, + salt: request.salt, + difficulty: request.difficulty, + } as WorkerRequest); + break; + case ChallengeStrategy.TargetNumber: + worker.postMessage({ + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.TargetNumber, + target: request.target, + salt: request.salt, + } as WorkerRequest); + break; + } + break; + case ChallengeAlgorithm.kCTF: worker.postMessage({ - strategy: ChallengeStrategy.kCTF, - challenge: request.challenge, + algorithm: ChallengeAlgorithm.kCTF, + salt: request.salt, + difficulty: request.difficulty, } as WorkerRequest); break; } @@ -329,11 +370,52 @@ export class PowCaptcha extends LitElement { // }; // break; // } - switch (this.challengeData.strategy) { - case ChallengeStrategy.kCTF: + switch (this.challengeData.algorithm) { + case ChallengeAlgorithm.SHA256: + switch (this.challengeData.strategy) { + case ChallengeStrategy.LeadingZeroes: + request = { + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.LeadingZeroes, + salt: this.challengeData.salt, + difficulty: this.challengeData.difficulty, + }; + break; + case ChallengeStrategy.TargetNumber: + request = { + algorithm: ChallengeAlgorithm.SHA256, + strategy: ChallengeStrategy.TargetNumber, + target: this.challengeData.target, + salt: this.challengeData.salt, + }; + break; + } + break; + case ChallengeAlgorithm.Argon2id: + switch (this.challengeData.strategy) { + case ChallengeStrategy.LeadingZeroes: + request = { + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.LeadingZeroes, + salt: this.challengeData.salt, + difficulty: this.challengeData.difficulty, + }; + break; + case ChallengeStrategy.TargetNumber: + request = { + algorithm: ChallengeAlgorithm.Argon2id, + strategy: ChallengeStrategy.TargetNumber, + target: this.challengeData.target, + salt: this.challengeData.salt, + }; + break; + } + break; + case ChallengeAlgorithm.kCTF: request = { - strategy: ChallengeStrategy.kCTF, - challenge: this.challengeData.challenge, + algorithm: ChallengeAlgorithm.kCTF, + salt: this.challengeData.salt, + difficulty: this.challengeData.difficulty, }; break; } @@ -346,9 +428,13 @@ export class PowCaptcha extends LitElement { // TODO: We need to do a better job of tracking solvers, so if one worker // errors out, we only error out if all workers have errored out. let worker_promises: Promise[] = []; - for (let worker of this.solverWorkers) { - // dispatch to all workers, func is async so it will not block - worker_promises.push(this.issueChallengeToWorker(worker, request)); + if (request.algorithm === ChallengeAlgorithm.kCTF) { + worker_promises.push(this.issueChallengeToWorker(this.solverWorkers[0], request)); + } else { + for (let worker of this.solverWorkers) { + // dispatch to all workers, func is async so it will not block + worker_promises.push(this.issueChallengeToWorker(worker, request)); + } } let solution = await Promise.race(worker_promises); @@ -370,7 +456,7 @@ export class PowCaptcha extends LitElement { await fetch(`${this.challengeUrl}/challenge`, { method: 'POST', body: JSON.stringify({ - challenge: this.challengeData.challenge, + salt: this.challengeData.salt, solution: solution.solution, }), headers: { @@ -382,7 +468,7 @@ export class PowCaptcha extends LitElement { this.dispatchEvent(new CustomEvent('impost:solved', { detail: { - challenge: this.challengeData.challenge, + salt: this.challengeData.salt, solution: solution.solution, }, bubbles: true, diff --git a/packages/widget/src/solver-worker.ts b/packages/widget/src/solver-worker.ts index 45d16ff..35bb9aa 100644 --- a/packages/widget/src/solver-worker.ts +++ b/packages/widget/src/solver-worker.ts @@ -8,7 +8,8 @@ import { WorkerResponseType, } from "./types/worker"; -import { type SolverModule, init_solver, solve } from '@impost/lib/solver'; +import { type SolverModule, init_solver, solve, type SolveParams } from '@impost/lib/solver'; +import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib'; let solver: SolverModule | null = null; @@ -59,9 +60,59 @@ onmessage = async (event: MessageEvent) => { return; } - let solution: string; + let solution: string | number; try { - solution = solve(solver, event.data.challenge); + let params = { + name: event.data.algorithm, + salt: event.data.salt, + }; + + switch (event.data.algorithm) { + case ChallengeAlgorithm.SHA256: + switch (event.data.strategy) { + case ChallengeStrategy.LeadingZeroes: + // @ts-ignore + params.strategy = ChallengeStrategy.LeadingZeroes; + // @ts-ignore + params.difficulty = event.data.difficulty; + break; + case ChallengeStrategy.TargetNumber: + // @ts-ignore + params.strategy = ChallengeStrategy.TargetNumber; + // @ts-ignore + params.target = event.data.target; + break; + } + break; + case ChallengeAlgorithm.Argon2id: + switch (event.data.strategy) { + case ChallengeStrategy.LeadingZeroes: + // @ts-ignore + params.strategy = ChallengeStrategy.LeadingZeroes; + // @ts-ignore + params.difficulty = event.data.difficulty; + break; + case ChallengeStrategy.TargetNumber: + // @ts-ignore + params.strategy = ChallengeStrategy.TargetNumber; + // @ts-ignore + params.target = event.data.target; + break; + } + break; + case ChallengeAlgorithm.kCTF: + // @ts-ignore + params.strategy = ChallengeStrategy.Null; + // @ts-ignore + params.difficulty = event.data.difficulty; + break; + } + + solution = solve(solver, params as SolveParams); + + if (event.data.algorithm !== ChallengeAlgorithm.kCTF) { + solution = Atomics.load(atomic_solution!, 0); + } } catch (error: any) { postMessage({ type: WorkerResponseType.Error, @@ -70,6 +121,7 @@ onmessage = async (event: MessageEvent) => { return; } + postMessage({ type: WorkerResponseType.Solution, solution, diff --git a/packages/widget/src/types/worker.ts b/packages/widget/src/types/worker.ts index 464ca19..c076f29 100644 --- a/packages/widget/src/types/worker.ts +++ b/packages/widget/src/types/worker.ts @@ -1,4 +1,4 @@ -import { ChallengeStrategy } from "@impost/lib"; +import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib"; export enum WorkerMessageType { Init = "init", @@ -12,37 +12,40 @@ interface WorkerInitRequest { sab: SharedArrayBuffer; } -// interface ChallengeLeadingZeroesSolveRequest { -// strategy: ChallengeStrategy.LeadingZeroes; -// salt: string; -// difficulty: number; -// } +interface ChallengeLeadingZeroesSolveRequest { + algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.LeadingZeroes; + salt: string; + difficulty: number; +} -// interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest { -// type: WorkerMessageType.Challenge; -// } +interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest { + type: WorkerMessageType.Challenge; +} -// interface ChallengeTargetNumberSolveRequest { -// strategy: ChallengeStrategy.TargetNumber; -// target: string; -// salt: string; -// } +interface ChallengeTargetNumberSolveRequest { + algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2id; + strategy: ChallengeStrategy.TargetNumber; + target: string; + salt: string; +} -// interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest { -// type: WorkerMessageType.Challenge; -// } +interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest { + type: WorkerMessageType.Challenge; +} interface ChallengekCTFSolveRequest { - strategy: ChallengeStrategy.kCTF; - challenge: string; + algorithm: ChallengeAlgorithm.kCTF; + salt: string; + difficulty: number; } interface WorkerChallengekCTFSolveRequest extends ChallengekCTFSolveRequest { type: WorkerMessageType.Challenge; } -export type ChallengeSolveRequest = ChallengekCTFSolveRequest; -type WorkerChallengeSolveRequest = WorkerChallengekCTFSolveRequest; +export type ChallengeSolveRequest = ChallengekCTFSolveRequest | ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest; +type WorkerChallengeSolveRequest = WorkerChallengekCTFSolveRequest | WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest; export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest; @@ -59,7 +62,7 @@ interface ErrorMessageResponse { interface SolutionMessageResponse { type: WorkerResponseType.Solution; - solution: string; + solution: string | number; } interface InitOkMessageResponse { diff --git a/solver/src/algorithms/algorithms.zig b/solver/src/algorithms/algorithms.zig new file mode 100644 index 0000000..81225b6 --- /dev/null +++ b/solver/src/algorithms/algorithms.zig @@ -0,0 +1,15 @@ +pub const Algorithm = enum(u8) { + sha256 = 0, + argon2 = 1, + kctf = 2, +}; + +pub const Strategy = enum(u8) { + null = 0, + leading_zeros = 1, + target_number = 2, +}; + +pub const SHA256 = @import("sha256.zig"); +pub const Argon2 = @import("argon2.zig"); +pub const kCTF = @import("kctf.zig"); diff --git a/solver/src/algorithms/argon2.zig b/solver/src/algorithms/argon2.zig new file mode 100644 index 0000000..c62d9ab --- /dev/null +++ b/solver/src/algorithms/argon2.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +var argon2_params = std.crypto.pwhash.argon2.Params{ + .t = 4, // time cost + .m = 256, // memory cost (in KiB) + .p = 1, // parallelism (this doesnt do anything because we are targeting wasm, and we do multithreading differently anyways) +}; + +const dk_len: usize = 32; // 16 or 32 byte key + +pub fn hash(allocator: Allocator, challenge: []const u8, nonce: []const u8) ![]u8 { + const derived = try allocator.alloc(u8, dk_len); + try std.crypto.pwhash.argon2.kdf(allocator, derived, nonce, challenge, argon2_params, .argon2id); + + return derived; +} diff --git a/solver/src/algorithms/kctf.zig b/solver/src/algorithms/kctf.zig new file mode 100644 index 0000000..5273f2a --- /dev/null +++ b/solver/src/algorithms/kctf.zig @@ -0,0 +1,169 @@ +// A PoW algorithm based on google's kCTF scheme +// https://google.github.io/kctf/ + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const math = std.math; +const Int = math.big.int.Managed; + +var managed_one: ?Int = null; + +fn get_bit(n: *Int, idx: usize) !bool { + if (n.len() < idx / @typeInfo(usize).int.bits) { + return false; + } + + var foo = try n.clone(); + defer foo.deinit(); + + try foo.shiftRight(n, idx); + try foo.bitAnd(&foo, &managed_one.?); + return foo.eql(managed_one.?); +} + +pub fn square_mod(n: *Int) !void { + const allocator = n.allocator; + try n.sqr(n); + + var high = try Int.init(allocator); + defer high.deinit(); + try high.shiftRight(n, 1279); // high = n >> 1279 + + var mask = try Int.init(allocator); + defer mask.deinit(); + + if (managed_one == null) { + managed_one = try Int.init(allocator); + try managed_one.?.set(1); + } + + try mask.set(1); + try mask.shiftLeft(&mask, 1279); + try mask.sub(&mask, &managed_one.?); + + try n.bitAnd(n, &mask); + + try n.add(n, &high); + + if (try get_bit(n, 1279)) { + // clear bit 1279 + var power_of_2 = try Int.init(allocator); + defer power_of_2.deinit(); + try power_of_2.set(1); + try power_of_2.shiftLeft(&power_of_2, 1279); + try n.sub(n, &power_of_2); + + // *n += 1; + try n.add(n, &managed_one.?); + } +} + +pub const Challenge = struct { + difficulty: usize, + salt: std.math.big.int.Managed, + + const Self = @This(); + + pub fn destroy(self: *Self, allocator: Allocator) void { + self.salt.deinit(); + allocator.destroy(self); + } + + pub fn from_string(allocator: Allocator, challenge: []const u8, difficulty: usize) !*Self { + var salt = try std.math.big.int.Managed.init(allocator); + errdefer salt.deinit(); + + const salt_str = challenge; + const salt_bytes_len = try std.base64.standard.Decoder.calcSizeForSlice(salt_str); + + std.log.info("salt_bytes_len: {d}\n", .{salt_bytes_len}); + + const salt_bytes = try allocator.alloc(u8, salt_bytes_len); + defer allocator.free(salt_bytes); + + try std.base64.standard.Decoder.decode(salt_bytes, salt_str); + + std.log.info("decoded salt: {any}\n", .{salt_bytes}); + + const usize_salt_bytes: []align(1) usize = std.mem.bytesAsSlice(usize, salt_bytes); + // TODO: the bytes are being read in as little endian, but need to be read in as big endian + std.log.info("usize_salt_bytes: {any}\n", .{usize_salt_bytes}); + try salt.ensureCapacity(usize_salt_bytes.len); + @memcpy(salt.limbs[0..usize_salt_bytes.len], usize_salt_bytes); + salt.setLen(usize_salt_bytes.len); + + const challenge_ptr = try allocator.create(Self); + errdefer challenge_ptr.destroy(allocator); + + challenge_ptr.* = Self{ + .difficulty = difficulty, + .salt = salt, + }; + + return challenge_ptr; + } + + pub fn encode(self: *Self, allocator: Allocator) ![]u8 { + const solution_base64_len = std.base64.standard.Encoder.calcSize(self.salt.len() * @sizeOf(usize)); + const dest = try allocator.alloc(u8, solution_base64_len); + defer allocator.free(dest); + @memset(dest, 0); + + const limbs_u8_buffer: []u8 = std.mem.sliceAsBytes(self.salt.limbs[0..self.salt.len()]); + const base64_str = std.base64.standard.Encoder.encode(dest, limbs_u8_buffer); + + return try std.fmt.allocPrint(allocator, "{s}", .{base64_str}); + } + + pub fn solve(self: *Self, allocator: Allocator) ![]u8 { + for (0..self.difficulty) |_| { + std.log.info("Solving challenge with difficulty {d}\n", .{self.difficulty}); + for (0..1277) |_| { + try square_mod(&self.salt); + } + try self.salt.bitXor(&self.salt, &managed_one.?); + } + + std.log.info("solved challenge: {any}\n", .{self}); + + return try self.encode(allocator); + } + + pub fn verify(self: *Self, allocator: Allocator, solution: *Challenge) !bool { + std.log.info("{d}", .{self.difficulty}); + std.log.info("{any} vs {any}\n", .{ self, solution }); + + if (managed_one == null) { + managed_one = try Int.init(allocator); + try managed_one.?.set(1); + } + + for (0..self.difficulty) |_| { + try solution.salt.bitXor(&solution.salt, &managed_one.?); + + try square_mod(&solution.salt); + } + + std.log.info("{any} vs {any}\n", .{ self, solution }); + + // I'm like 99.999% sure this can NEVER happen, but its how the solution that I translated from did it so that's + // how I will do it + if (self.salt.eql(solution.salt)) { + return true; + } + + var foo = try std.math.big.int.Managed.initSet(allocator, 2); + defer foo.deinit(); + try foo.pow(&foo, 1279); + try foo.sub(&foo, &managed_one.?); + try foo.sub(&foo, &self.salt); + + if (foo.eql(solution.salt)) { + std.log.info("challenge solved!\n", .{}); + return true; + } + + return false; + } +}; diff --git a/solver/src/algorithms/sha256.zig b/solver/src/algorithms/sha256.zig new file mode 100644 index 0000000..0eacf52 --- /dev/null +++ b/solver/src/algorithms/sha256.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub fn hash(allocator: Allocator, data: []const u8) ![]u8 { + const output_hash = try allocator.alloc(u8, std.crypto.hash.sha2.Sha256.digest_length); + std.crypto.hash.sha2.Sha256.hash(data, @ptrCast(output_hash), .{}); + + return output_hash; +} diff --git a/solver/src/hasher.zig b/solver/src/hasher.zig deleted file mode 100644 index 888b38e..0000000 --- a/solver/src/hasher.zig +++ /dev/null @@ -1,31 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -var argon2_params = std.crypto.pwhash.argon2.Params{ - .t = 4, // time cost - .m = 256, // memory cost (in KiB) - .p = 1, // parallelism (this doesnt do anything because we are targeting wasm, and we do multithreading differently anyways) -}; - -const dk_len: usize = 32; // 16 or 32 byte key -var derived: [dk_len]u8 = undefined; -var buffer_hash_hex: [64]u8 = undefined; - -fn bytesToHex(bytes: []const u8, output: []u8) void { - const hex_chars = "0123456789abcdef"; - var i: usize = 0; - while (i < bytes.len) : (i += 1) { - output[i * 2] = hex_chars[(bytes[i] >> 4)]; - output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F]; - } -} - -pub fn hash(allocator: Allocator, challenge: []const u8, nonce: []const u8) ![]u8 { - try std.crypto.pwhash.argon2.kdf(allocator, &derived, nonce, challenge, argon2_params, .argon2id); - - var hash_bytes: [32]u8 = undefined; - std.crypto.hash.sha2.Sha256.hash(&derived, @ptrCast(hash_bytes[0..].ptr), .{}); - - bytesToHex(&hash_bytes, &buffer_hash_hex); - return buffer_hash_hex[0..]; -} diff --git a/solver/src/kctf.zig b/solver/src/kctf.zig deleted file mode 100644 index 957d6e9..0000000 --- a/solver/src/kctf.zig +++ /dev/null @@ -1,215 +0,0 @@ -// A PoW algorithm based on google's kCTF scheme -// https://google.github.io/kctf/ - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const math = std.math; -const Int = math.big.int.Managed; - -var managed_one: ?Int = null; - -fn get_bit(n: *Int, idx: usize) !bool { - if (n.len() < idx / @typeInfo(usize).int.bits) { - return false; - } - - var foo = try n.clone(); - defer foo.deinit(); - - try foo.shiftRight(n, idx); - try foo.bitAnd(&foo, &managed_one.?); - return foo.eql(managed_one.?); -} - -pub fn square_mod(n: *Int) !void { - const allocator = n.allocator; - try n.sqr(n); - - var high = try Int.init(allocator); - defer high.deinit(); - try high.shiftRight(n, 1279); // high = n >> 1279 - - var mask = try Int.init(allocator); - defer mask.deinit(); - - if (managed_one == null) { - managed_one = try Int.init(allocator); - try managed_one.?.set(1); - } - - try mask.set(1); - try mask.shiftLeft(&mask, 1279); - try mask.sub(&mask, &managed_one.?); - - try n.bitAnd(n, &mask); - - try n.add(n, &high); - - if (try get_bit(n, 1279)) { - // clear bit 1279 - var power_of_2 = try Int.init(allocator); - defer power_of_2.deinit(); - try power_of_2.set(1); - try power_of_2.shiftLeft(&power_of_2, 1279); - try n.sub(n, &power_of_2); - - // *n += 1; - try n.add(n, &managed_one.?); - } -} - -pub const Version = "s"; - -pub const Challenge = struct { - difficulty: ?u32, - salt: std.math.big.int.Managed, - - const Self = @This(); - - pub fn destroy(self: *Self, allocator: Allocator) void { - self.salt.deinit(); - allocator.destroy(self); - } - - pub fn encode(self: *Self, allocator: Allocator) ![]u8 { - const solution_base64_len = std.base64.standard.Encoder.calcSize(self.salt.len() * @sizeOf(usize)); - const dest = try allocator.alloc(u8, solution_base64_len); - defer allocator.free(dest); - @memset(dest, 0); - - const limbs_u8_buffer: []u8 = std.mem.sliceAsBytes(self.salt.limbs[0..self.salt.len()]); - const base64_str = std.base64.standard.Encoder.encode(dest, limbs_u8_buffer); - - return try std.fmt.allocPrint(allocator, "{s}", .{base64_str}); - } -}; - -pub fn decode(allocator: Allocator, challenge: []const u8) !*Challenge { - var parts = std.mem.splitAny(u8, challenge, "."); - if (parts.next()) |part| { - if (!std.mem.eql(u8, part, Version)) { - return error.InvalidChallenge; - } - } else { - return error.InvalidChallenge; - } - - var difficulty: ?u32 = null; - var next_part = parts.next() orelse return error.InvalidChallenge; - if (parts.peek()) |_| { - // must be .. - const difficulty_bytes = try allocator.alloc(u8, try std.base64.standard.Decoder.calcSizeForSlice(next_part)); - defer allocator.free(difficulty_bytes); - - try std.base64.standard.Decoder.decode(difficulty_bytes, next_part); - - std.log.info("Decoded difficulty bytes: {any}\n", .{difficulty_bytes}); - - var difficulty_array: [4]u8 = .{0} ** 4; - if (difficulty_bytes.len > 4) { - const split_idx = difficulty_bytes.len - 4; - for (difficulty_bytes[0..split_idx]) |byte| { - if (byte != 0) return error.DifficultyTooLarge; - } - - @memcpy(&difficulty_array, difficulty_bytes[split_idx..]); - difficulty = std.mem.readInt(u32, &difficulty_array, .big); - } else { - const start_idx = 4 - difficulty_bytes.len; - @memcpy(&difficulty_array, difficulty_bytes[start_idx..]); - difficulty = std.mem.readInt(u32, &difficulty_array, .big); - } - - next_part = parts.next() orelse return error.InvalidChallenge; - } - - var salt = try std.math.big.int.Managed.init(allocator); - errdefer salt.deinit(); - - const salt_str = next_part; - const salt_bytes_len = try std.base64.standard.Decoder.calcSizeForSlice(salt_str); - - std.log.info("salt_bytes_len: {d}\n", .{salt_bytes_len}); - - const salt_bytes = try allocator.alloc(u8, salt_bytes_len); - defer allocator.free(salt_bytes); - - try std.base64.standard.Decoder.decode(salt_bytes, salt_str); - - std.log.info("decoded salt: {any}\n", .{salt_bytes}); - - const usize_salt_bytes: []align(1) usize = std.mem.bytesAsSlice(usize, salt_bytes); - // TODO: the bytes are being read in as little endian, but need to be read in as big endian - std.log.info("usize_salt_bytes: {any}\n", .{usize_salt_bytes}); - try salt.ensureCapacity(usize_salt_bytes.len); - @memcpy(salt.limbs[0..usize_salt_bytes.len], usize_salt_bytes); - salt.setLen(usize_salt_bytes.len); - - const challenge_ptr = try allocator.create(Challenge); - errdefer challenge_ptr.destroy(allocator); - - challenge_ptr.* = Challenge{ - .difficulty = difficulty, - .salt = salt, - }; - - return challenge_ptr; -} - -pub fn solve(allocator: Allocator, challenge: *Challenge) ![]u8 { - if (challenge.difficulty == null) { - return error.InvalidChallenge; - } - - for (0..challenge.difficulty.?) |_| { - std.log.info("Solving challenge with difficulty {d}\n", .{challenge.difficulty.?}); - for (0..1277) |_| { - try square_mod(&challenge.salt); - } - try challenge.salt.bitXor(&challenge.salt, &managed_one.?); - } - - std.log.info("solved challenge: {any}\n", .{challenge}); - - return try challenge.encode(allocator); -} - -pub fn check(allocator: Allocator, challenge: *Challenge, solution: *Challenge) !bool { - std.log.info("{d}", .{challenge.difficulty.?}); - std.log.info("{any} vs {any}\n", .{ challenge, solution }); - - if (challenge.difficulty == null) { - return error.InvalidChallenge; - } - - if (managed_one == null) { - managed_one = try Int.init(allocator); - try managed_one.?.set(1); - } - - for (0..challenge.difficulty.?) |_| { - try solution.salt.bitXor(&solution.salt, &managed_one.?); - - try square_mod(&solution.salt); - } - - std.log.info("{any} vs {any}\n", .{ challenge, solution }); - - if (challenge.salt.eql(solution.salt)) { - return true; - } - - var foo = try std.math.big.int.Managed.initSet(allocator, 2); - defer foo.deinit(); - try foo.pow(&foo, 1279); - try foo.sub(&foo, &managed_one.?); - try foo.sub(&foo, &challenge.salt); - - if (foo.eql(solution.salt)) { - std.log.info("challenge solved!\n", .{}); - return true; - } - - return false; -} diff --git a/solver/src/solver.zig b/solver/src/solver.zig index 6a40ca5..5801c7a 100644 --- a/solver/src/solver.zig +++ b/solver/src/solver.zig @@ -2,11 +2,16 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const kCTF = @import("kctf.zig"); +const algorithms = @import("algorithms/algorithms.zig"); +const utils = @import("utils.zig"); var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; var allocator = gpa.allocator(); +extern fn __get_solution() i32; +extern fn __set_solution(value: i32) void; +extern fn __cmpxchg_solution(old: i32, new: i32) i32; +extern fn __fetch_add_nonce(value: i32) i32; extern fn __log(str_ptr: usize, str_len: usize) void; fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void { @@ -15,9 +20,18 @@ fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), com return; } + const log_level_str = switch (level) { + .err => "Error: ", + .warn => "Warning: ", + .info => "Info: ", + .debug => "Debug: ", + }; + const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return; - __log(@intFromPtr(formatted.ptr), formatted.len); + const log_str = std.fmt.allocPrint(allocator, "{s}{s}", .{ log_level_str, formatted }) catch return; allocator.free(formatted); + __log(@intFromPtr(log_str.ptr), log_str.len); + allocator.free(log_str); } pub const std_options: std.Options = .{ .logFn = log }; @@ -34,42 +48,180 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void { } } -// value_ptr is a string to the entire encoded challenge string (e.g. "s.xxxxxxxxx.xxxxxxx") -export fn solve(value_ptr: [*]u8, value_len: usize) usize { +/// Both SHA256 and Argon2 are thread safe and are explicitly designed to be used in a multithreaded environment. +/// kCTF is designed only to be used in a single threaded environment. It does not use the same nonce atomics, +/// and duplicates work if solved across multiple threads. +/// +/// If a target is not needed for the strategy, target_ptr and target_len should be 0. +export fn solve(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, salt_ptr: [*]u8, salt_len: usize, difficulty: usize, target_ptr: [*]u8, target_len: usize) isize { + std.log.info("Solve called with difficulty {d}\n", .{difficulty}); + std.log.info("Using algorithm {s} and strategy {s}\n", .{ @tagName(algorithm), @tagName(strategy) }); + + switch (algorithm) { + algorithms.Algorithm.sha256 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len), + algorithms.Algorithm.argon2 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len), + algorithms.Algorithm.kctf => { + if (strategy != algorithms.Strategy.null) { + std.log.err("kCTF does not support a strategy", .{}); + return -1; + } + + return solve_kctf(salt_ptr, salt_len, difficulty); + }, + } +} + +fn solve_argon2_or_sha256(salt_ptr: [*]u8, salt_len: usize, difficulty: usize, algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, target_ptr: [*]u8, target_len: usize) isize { + if (strategy == algorithms.Strategy.null) { + std.log.err("Argon2 needs a strategy", .{}); + return -1; + } + + if (strategy == .leading_zeros) { + if (difficulty < 1 or difficulty > 64) { + std.log.err("Argon2 difficulty must be between 1 and 64 when using leading_zeros", .{}); + return -1; + } + } + + const salt_slice = salt_ptr[0..salt_len]; + var target_slice: ?[]u8 = null; + if (@intFromPtr(target_ptr) != 0) { + target_slice = target_ptr[0..target_len]; + } + + if (strategy == .target_number and target_slice == null) { + std.log.err("A target must be specified when using the target_number strategy", .{}); + return -1; + } + + // const max_nonce_iterations: u64 = 1_000_000_000; + const max_nonce_iterations: u64 = 100_000; + + std.log.info("Solve called with salt {s}\n", .{salt_slice}); + + // 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations) + var input_buffer: []u8 = allocator.alloc(u8, salt_len + 9) catch { + std.log.err("Out of memory", .{}); + return -1; + }; + // dont leak memory :pepega: + defer allocator.free(input_buffer); + + @memcpy(input_buffer[0..salt_len], salt_slice); + + var nonce = __fetch_add_nonce(1); + var hex_encoder = utils.HexEncoder{}; + var input: []u8 = undefined; + + while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) { + if (__get_solution() != -1) { + // solution has already been found, no point in continuing + return 0; + } + + const nonce_str = std.fmt.bufPrint(input_buffer[salt_len..], "{d}", .{nonce}) catch { + std.log.err("Error formatting nonce", .{}); + return -1; + }; + + if (algorithm == .argon2) { + input = algorithms.Argon2.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch { + std.log.err("Error hashing salt", .{}); + return -1; + }; + } else { + input = input_buffer[0 .. salt_len + nonce_str.len]; + } + + const hash_hex_slice = algorithms.SHA256.hash(allocator, input) catch { + std.log.err("Error hashing key", .{}); + return -1; + }; + + if (algorithm == .argon2) { + allocator.free(input); + } + + switch (strategy) { + .leading_zeros => { + _ = hex_encoder.encode(hash_hex_slice); + allocator.free(hash_hex_slice); + if (hex_encoder.countZeroes(difficulty)) { + // Found a solution! + if (__cmpxchg_solution(-1, nonce) == -1) { + // we found a solution, and we are the first to do so + return nonce; + } else { + // we found a solution, but we are not the first to do so + return 0; + } + } + }, + .target_number => { + const hex = hex_encoder.encode(hash_hex_slice); + allocator.free(hash_hex_slice); + if (std.mem.eql(u8, hex, target_slice.?)) { + // Found a solution! + if (__cmpxchg_solution(-1, nonce) == -1) { + // we found a solution, and we are the first to do so + return nonce; + } else { + // we found a solution, but we are not the first to do so + return 0; + } + } + }, + else => { + std.log.err("Invalid strategy: {s}", .{@tagName(strategy)}); + return -1; + }, + } + } + + return 0; +} + +// value_ptr is a just the base64 challenge string (e.g. "xxxxxxxxx==") +fn solve_kctf(value_ptr: [*]u8, value_len: usize, difficulty: usize) isize { + if (difficulty < 1) { + std.log.err("KCTF difficulty must be at least 1", .{}); + return -1; + } + const challenge_slice = value_ptr[0..value_len]; std.log.info("Solve called with challenge {s}\n", .{challenge_slice}); - const challenge = kCTF.decode(allocator, challenge_slice) catch |err| { + const challenge = algorithms.kCTF.Challenge.from_string(allocator, challenge_slice, difficulty) catch |err| { std.log.info("Error decoding challenge: {s}\n", .{@errorName(err)}); - return 0; + return -1; }; defer challenge.destroy(allocator); std.log.info("decoded challenge {any}\n", .{challenge}); - const solution = kCTF.solve(allocator, challenge) catch |err| { + const solution = challenge.solve(allocator) catch |err| { std.log.info("Error solving challenge: {s}\n", .{@errorName(err)}); - return 0; + return -1; }; std.log.info("Solution: {s}\n", .{solution}); const output_ptr = allocator.alloc(u8, solution.len + 4) catch return 0; - var output_slice = output_ptr[0 .. solution.len + 4]; + var output_slice = output_ptr[0 .. solution.len + 2]; if (output_slice.len - 2 > std.math.maxInt(u16)) { - return 0; + return -1; } const output_len: u16 = @intCast(output_slice.len - 2); // convert to little endian output_slice[0] = @intCast(output_len & 0xFF); // LSB output_slice[1] = @intCast(output_len >> 8); // MSB - @memcpy(output_slice[2..4], "s."); - @memcpy(output_slice[4 .. 4 + solution.len], solution); + @memcpy(output_slice[2 .. 2 + solution.len], solution); allocator.free(solution); - return @intFromPtr(output_ptr.ptr); + return @intCast(@intFromPtr(output_ptr.ptr)); } pub fn main() anyerror!void { @@ -77,12 +229,79 @@ pub fn main() anyerror!void { var args = try std.process.argsAlloc(allocator); if (args.len < 2) { - std.log.err("Usage: zig run src/kctf.zig ", .{}); + std.log.err("Usage: {s} [options] ", .{args[0]}); return; } - const challenge = try kCTF.decode(allocator, args[1]); - const solution = try kCTF.solve(allocator, challenge); + var algorithm: ?algorithms.Algorithm = null; + var strategy: algorithms.Strategy = algorithms.Strategy.null; + var target: ?[]u8 = null; + + if (std.mem.eql(u8, args[1], "sha256")) { + algorithm = algorithms.Algorithm.sha256; + } else if (std.mem.eql(u8, args[1], "argon2")) { + algorithm = algorithms.Algorithm.argon2; + } else if (std.mem.eql(u8, args[1], "kctf")) { + algorithm = algorithms.Algorithm.kctf; + } + + var i: usize = 2; + while (i < args.len) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "--strategy")) { + if (args.len <= i + 1) { + std.log.err("Expected strategy after --strategy", .{}); + return; + } + + if (std.mem.eql(u8, args[i + 1], "leading_zeros")) { + strategy = algorithms.Strategy.leading_zeros; + } + + if (std.mem.eql(u8, args[i + 1], "target_number")) { + strategy = algorithms.Strategy.target_number; + } + + if (strategy == .null) { + std.log.err("Invalid strategy: {s}", .{args[i + 1]}); + return; + } + + i += 1; + } + + if (std.mem.eql(u8, arg, "--target")) { + if (args.len <= i + 1) { + std.log.err("Expected target after --target", .{}); + return; + } + + target = args[i + 1]; + i += 1; + } + + if (std.mem.eql(u8, arg, "--help")) { + std.log.info("Options:\n", .{}); + std.log.info(" --strategy : Specify the strategy to use. This only applies to some algorithms.\n", .{}); + std.log.info(" --target : Specify the target hash when using the target_number strategy.\n", .{}); + std.log.info(" --help: Print this help message\n", .{}); + std.log.info("Usage: {s} [options] ", .{args[0]}); + return; + } + } + + if (strategy == .null and algorithm != .kctf) { + std.log.warn("No strategy specified, defaulting to leading_zeros", .{}); + strategy = algorithms.Strategy.leading_zeros; + } + + if (strategy == .target_number and target == null) { + std.log.err("A target must be specified when using the target_number strategy", .{}); + return; + } + + const challenge = try algorithms.kCTF.decode(allocator, args[1]); + const solution = try algorithms.kCTF.solve(allocator, challenge); std.log.info("Solution: {s}", .{solution}); } diff --git a/solver/src/utils.zig b/solver/src/utils.zig new file mode 100644 index 0000000..fc736ae --- /dev/null +++ b/solver/src/utils.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const HexEncoder = struct { + scratch: [64]u8 = undefined, + scratch_set: bool = false, + + const Self = @This(); + + pub fn encode(self: *Self, bytes: []const u8) []u8 { + self.scratch_set = true; + + bytesToHex(bytes, &self.scratch); + + return &self.scratch; + } + + // counts the number of leading hexidecimal zeroes in the scratch buffer + // which is set by encode + pub fn countZeroes(self: *Self, zeroes: usize) bool { + if (!self.scratch_set) { + return false; + } + + if (zeroes > 64 or zeroes == 0) { + return false; + } + + var i: usize = 0; + while (i < zeroes) : (i += 1) { + if (self.scratch[i] != '0') { + return false; + } + } + + return true; + } +}; + +fn bytesToHex(bytes: []const u8, output: []u8) void { + const hex_chars = "0123456789abcdef"; + var i: usize = 0; + while (i < bytes.len) : (i += 1) { + output[i * 2] = hex_chars[(bytes[i] >> 4)]; + output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F]; + } +} diff --git a/solver/src/validator.zig b/solver/src/validator.zig index d3bee4f..dbef391 100644 --- a/solver/src/validator.zig +++ b/solver/src/validator.zig @@ -2,7 +2,8 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const kCTF = @import("kctf.zig"); +const algorithms = @import("algorithms/algorithms.zig"); +const utils = @import("utils.zig"); var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; var allocator = gpa.allocator(); @@ -34,26 +35,81 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void { } } -fn bytesToHex(bytes: []const u8, buf: []u8) void { - const hex_chars = "0123456789abcdef"; - var i: usize = 0; - while (i < bytes.len) : (i += 1) { - buf[i * 2] = hex_chars[(bytes[i] >> 4)]; - buf[i * 2 + 1] = hex_chars[bytes[i] & 0x0F]; +export fn validate(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, nonce: usize, difficulty: usize) bool { + switch (algorithm) { + algorithms.Algorithm.sha256 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.sha256, strategy), + algorithms.Algorithm.argon2 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.argon2, strategy), + algorithms.Algorithm.kctf => return validate_kctf(challenge_ptr, challenge_len, solution_ptr, solution_len, difficulty), } } -// challenge_ptr should look like s.. -// solution_ptr should look like s. -export fn validate(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize) bool { +fn validate_argon2_or_sha256(challenge_ptr: [*]u8, challenge_len: usize, nonce: usize, target_ptr: [*]u8, target_len: usize, difficulty: usize, algorithm: algorithms.Algorithm, strategy: algorithms.Strategy) bool { + if (strategy == algorithms.Strategy.null) { + return false; + } + + if (strategy == .leading_zeros) { + if (difficulty < 1 or difficulty > 64) { + return false; + } + } + + const challenge_slice = challenge_ptr[0..challenge_len]; + const nonce_slice = std.fmt.allocPrint(allocator, "{d}", .{nonce}) catch return false; + + var target_slice: ?[]u8 = null; + if (@intFromPtr(target_ptr) != 0) { + target_slice = target_ptr[0..target_len]; + } + + if (strategy == .target_number and target_slice == null) { + return false; + } + + const input_slice = allocator.alloc(u8, challenge_len + nonce_slice.len) catch return false; + defer allocator.free(input_slice); + @memcpy(input_slice[0..challenge_len], challenge_slice); + @memcpy(input_slice[challenge_len..], nonce_slice); + + var input: []u8 = undefined; + if (algorithm == .argon2) { + input = algorithms.Argon2.hash(allocator, input_slice[0..challenge_len], input_slice[challenge_len .. challenge_len + nonce_slice.len]) catch return false; + defer allocator.free(input); + } else { + input = input_slice[0 .. challenge_len + nonce_slice.len]; + } + + var hex_encoder = utils.HexEncoder{}; + const hash_hex_slice = algorithms.SHA256.hash(allocator, input) catch return false; + defer allocator.free(hash_hex_slice); + + switch (strategy) { + .leading_zeros => { + _ = hex_encoder.encode(hash_hex_slice); + if (hex_encoder.countZeroes(difficulty)) { + return true; + } + }, + .target_number => { + if (std.mem.eql(u8, hex_encoder.encode(hash_hex_slice), target_slice.?)) { + return true; + } + }, + else => unreachable, + } + + return false; +} + +fn validate_kctf(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, difficulty: usize) bool { const challenge_buf = challenge_ptr[0..challenge_len]; const solution_buf = solution_ptr[0..solution_len]; std.log.info("Validate called with challenge {s} and solution {s}\n", .{ challenge_buf, solution_buf }); - const challenge = kCTF.decode(allocator, challenge_buf) catch return false; + const challenge = algorithms.kCTF.Challenge.from_string(allocator, challenge_buf, difficulty) catch return false; std.log.info("decoded challenge {any}\n", .{challenge}); - const solution = kCTF.decode(allocator, solution_buf) catch return false; + const solution = algorithms.kCTF.Challenge.from_string(allocator, solution_buf, difficulty) catch return false; defer { challenge.destroy(allocator); solution.destroy(allocator); @@ -61,33 +117,71 @@ export fn validate(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*] std.log.info("decoded challenge and solution\n", .{}); - const is_valid = kCTF.check(allocator, challenge, solution) catch return false; + const is_valid = challenge.verify(allocator, solution) catch return false; return is_valid; } -pub fn main() anyerror!void { - if (comptime builtin.cpu.arch == .wasm32) return; +export fn hash(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, algorithm: algorithms.Algorithm) u64 { + const challenge = challenge_ptr[0..challenge_len]; + const nonce = nonce_ptr[0..nonce_len]; - const args = try std.process.argsAlloc(allocator); - if (args.len < 3) { - std.log.err("Usage: zig run src/validator.zig ", .{}); - return; + var hash_slice: []u8 = undefined; + switch (algorithm) { + algorithms.Algorithm.sha256 => { + const input_slice = allocator.alloc(u8, challenge_len + nonce_len) catch return 0; + defer allocator.free(input_slice); + @memcpy(input_slice[0..challenge_len], challenge); + @memcpy(input_slice[challenge_len..], nonce); + + hash_slice = algorithms.SHA256.hash(allocator, input_slice[0 .. challenge_len + nonce_len]) catch return 0; + }, + algorithms.Algorithm.argon2 => { + const argon_key = algorithms.Argon2.hash(allocator, challenge, nonce) catch return 0; + defer allocator.free(argon_key); + + hash_slice = algorithms.SHA256.hash(allocator, argon_key) catch return 0; + }, + else => return 0, } - const challenge = try kCTF.decode(allocator, args[1]); - defer challenge.destroy(allocator); + var hex_encoder = utils.HexEncoder{}; + const hex_slice = hex_encoder.encode(hash_slice); + // hex_slice is stack allocated, therefore, if we pass it to the caller without copying it onto the heap, we are + // potentially (and likely) sending garbage memory to the caller + const heap_hex_slice = allocator.dupe(u8, hex_slice) catch return 0; - const solution = try kCTF.decode(allocator, args[2]); - defer solution.destroy(allocator); - - std.log.info("Challenge: {any}\n", .{challenge}); - std.log.info("Solution: {any}\n", .{solution}); - - const is_valid = kCTF.check(allocator, challenge, solution) catch |err| { - std.log.info("Error checking challenge: {s}\n", .{@errorName(err)}); - return; - }; - - std.log.info("Is valid: {}\n", .{is_valid}); + // bs to get the compiler to not whine about hash_slice.len being a u5 annd thus cannot be shifted by 32 + var ret: u64 = heap_hex_slice.len; + ret <<= 32; + ret |= @intFromPtr(heap_hex_slice.ptr); + allocator.free(hash_slice); + return ret; +} + +pub fn main() anyerror!void { + // TODO + // if (comptime builtin.cpu.arch == .wasm32) return; + + // const args = try std.process.argsAlloc(allocator); + // if (args.len < 3) { + // std.log.err("Usage: zig run src/validator.zig ", .{}); + // return; + // } + + // const challenge = try kCTF.decode(allocator, args[1]); + // defer challenge.destroy(allocator); + + // const solution = try kCTF.decode(allocator, args[2]); + // defer solution.destroy(allocator); + + // std.log.info("Challenge: {any}\n", .{challenge}); + // std.log.info("Solution: {any}\n", .{solution}); + + // const is_valid = kCTF.check(allocator, challenge, solution) catch |err| { + // std.log.info("Error checking challenge: {s}\n", .{@errorName(err)}); + // return; + // }; + + // std.log.info("Is valid: {}\n", .{is_valid}); }