diff --git a/example-app/app/pages/index.vue b/example-app/app/pages/index.vue index e1609f6..b4072ee 100644 --- a/example-app/app/pages/index.vue +++ b/example-app/app/pages/index.vue @@ -1,411 +1,37 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/example-app/app/pages/index.vue.old b/example-app/app/pages/index.vue.old new file mode 100644 index 0000000..e1609f6 --- /dev/null +++ b/example-app/app/pages/index.vue.old @@ -0,0 +1,411 @@ + + + + + \ No newline at end of file diff --git a/example-app/app/pages/widget.vue b/example-app/app/pages/widget.vue deleted file mode 100644 index af5a7e7..0000000 --- a/example-app/app/pages/widget.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - \ No newline at end of file diff --git a/example-app/config.toml b/example-app/config.toml index 175d496..e3e4da3 100644 --- a/example-app/config.toml +++ b/example-app/config.toml @@ -1,7 +1,10 @@ -strategy = "target_number" +strategy = "kctf" [leading_zeroes] difficulty = 4 [target_number] max_number = 10000 + +[kctf] +difficulty = 100 diff --git a/example-app/package-lock.json b/example-app/package-lock.json index 133031d..c7e8194 100644 --- a/example-app/package-lock.json +++ b/example-app/package-lock.json @@ -6,8 +6,8 @@ "": { "name": "hello-nuxt", "dependencies": { - "@impost/lib": "file:../packages/lib", - "@impost/widget": "file:../packages/widget", + "@impost/lib": "^0.1.0", + "@impost/widget": "^0.1.0", "@lit/reactive-element": "^2.1.1", "js-toml": "^1.0.2", "lit-element": "^4.2.1", @@ -41,6 +41,9 @@ "name": "@impost/lib", "version": "0.1.0", "license": "BSL-1.0", + "dependencies": { + "uuidv7": "^1.0.2" + }, "devDependencies": { "oxc-minify": "^0.97.0", "tslib": "^2.6.2", diff --git a/example-app/package.json b/example-app/package.json index 2d301c0..89ed353 100644 --- a/example-app/package.json +++ b/example-app/package.json @@ -16,8 +16,8 @@ "nuxt": "latest", "nuxt-ssr-lit": "1.6.32", "zod": "^4.1.12", - "@impost/lib": "file:../packages/lib", - "@impost/widget": "file:../packages/widget" + "@impost/lib": "^0.1.0", + "@impost/widget": "^0.1.0" }, "devDependencies": { "@types/node": "^24.10.0", diff --git a/example-app/server/api/pow/challenge.get.ts b/example-app/server/api/pow/challenge.get.ts index 607cc42..cb0f522 100644 --- a/example-app/server/api/pow/challenge.get.ts +++ b/example-app/server/api/pow/challenge.get.ts @@ -6,19 +6,28 @@ 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.LeadingZeroes: + case ChallengeStrategy.kCTF: 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, + difficulty: config.kctf.difficulty, }; break; } @@ -31,10 +40,10 @@ export default defineEventHandler(async () => { }); } - outstandingChallenges.set(challenge.salt, { + outstandingChallenges.set(challenge.challenge, { challenge, timeout: setTimeout(() => { - console.log("Challenge timed out:", challenge.salt); - outstandingChallenges.delete(challenge.salt); + console.log("Challenge timed out:", challenge.challenge); + outstandingChallenges.delete(challenge.challenge); }, CHALLENGE_TIMEOUT_MS) }); diff --git a/example-app/server/api/pow/challenge.post.ts b/example-app/server/api/pow/challenge.post.ts index e140f92..58d39ae 100644 --- a/example-app/server/api/pow/challenge.post.ts +++ b/example-app/server/api/pow/challenge.post.ts @@ -4,12 +4,13 @@ import * as z from 'zod'; import { outstandingChallenges } from '~~/server/utils/pow'; const challengeSchema = z.object({ - challenge: z.string(), - nonce: z.string() + challenge: z.string().startsWith("s."), + solution: z.string().startsWith("s.") }) // 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) { @@ -19,19 +20,25 @@ export default defineEventHandler(async (event) => { }) } - let target = body.data.challenge; - let nonce = body.data.nonce; + let { challenge, solution } = body.data; + + const outstanding_challenge = outstandingChallenges.get(challenge); + if (outstanding_challenge === undefined) { + throw createError({ + statusCode: 400, + statusMessage: 'Challenge not found' + }) + } // check if the challenge is valid - let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, { - challenge: target, - nonce: nonce - }); + const challenge_valid = await validate_challenge(outstanding_challenge.challenge, solution); + + console.log("CHALLENGE VALID", challenge_valid); if (challenge_valid) { // clear the challenge - clearTimeout(outstandingChallenges.get(target)!.timeout); - outstandingChallenges.delete(target); + clearTimeout(outstandingChallenges.get(challenge)!.timeout); + outstandingChallenges.delete(challenge); return { message: 'Challenge solved' diff --git a/example-app/server/utils/config.ts b/example-app/server/utils/config.ts index 8bab744..e2ad38a 100644 --- a/example-app/server/utils/config.ts +++ b/example-app/server/utils/config.ts @@ -3,26 +3,31 @@ import { load } from 'js-toml'; import z from 'zod'; import { 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 LeadingZeroesSchema = z.object({ +// strategy: z.literal(ChallengeStrategy.LeadingZeroes), +// leading_zeroes: z.object({ +// difficulty: z.number().int().min(1).max(64), +// }), +// }); + +// const TargetNumberSchema = z.object({ +// 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), + kctf: z.object({ + difficulty: z.number().int().min(1), }), }); -const TargetNumberSchema = z.object({ - strategy: z.literal(ChallengeStrategy.TargetNumber), - target_number: z.object({ - max_number: z.number().int().min(1).max(100_000), - }), -}); - - export type Config = z.infer; export const Config = z.discriminatedUnion('strategy', [ - LeadingZeroesSchema, - TargetNumberSchema, + kCTFSchema, ]); export let config: Config; diff --git a/justfile b/justfile index 3d10f4c..c47db72 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,14 @@ -build: build-widget - wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz" -zig-build-args := "--release=fast" +zig-build-args := "--release=fast -Dtarget=wasm32-freestanding -Dcpu=generic+bulk_memory+bulk_memory_opt+simd128+tail_call" npm-runner := "npm" +[working-directory: "example-app"] +playground: build + {{npm-runner}} run dev + +build: build-widget + [working-directory: "solver"] build-wasm: zig build {{zig-build-args}} diff --git a/packages/lib/package-lock.json b/packages/lib/package-lock.json index d643549..e66168c 100644 --- a/packages/lib/package-lock.json +++ b/packages/lib/package-lock.json @@ -2058,7 +2058,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2332,7 +2331,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2383,7 +2381,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 01081a7..9cfa23b 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -1,28 +1,44 @@ +import { UUID } from "uuidv7"; + export enum ChallengeAlgorithm { Argon2id = "argon2id", } export enum ChallengeStrategy { + kCTF = "kctf", LeadingZeroes = "leading_zeroes", TargetNumber = "target_number", } // 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 +// export interface ChallengeLeadingZeroes { +// algorithm: ChallengeAlgorithm; +// 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 +// } + +export interface InnerChallengekCTF { + strategy: ChallengeStrategy.kCTF; + salt: UUID; // UUIDv7 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 +export interface ChallengekCTF { + strategy: ChallengeStrategy.kCTF; + challenge: string; } -export type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber; \ No newline at end of file +export type InnerChallenge = InnerChallengekCTF; + +export type Challenge = ChallengekCTF; \ No newline at end of file diff --git a/packages/lib/src/solver.ts b/packages/lib/src/solver.ts index 7161566..0ba4144 100644 --- a/packages/lib/src/solver.ts +++ b/packages/lib/src/solver.ts @@ -3,8 +3,10 @@ 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_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, "memory": WebAssembly.Memory; } @@ -17,6 +19,7 @@ export type SolverEnv = { __set_solution: (value: number) => void; __cmpxchg_solution: (expected: number, replacement: number) => number; __fetch_add_nonce: (value: number) => number; + __log: (str_ptr: number, str_len: number) => void; }; export async function get_wasm_module(): Promise { @@ -29,64 +32,35 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P }) as unknown as SolverModule; } -export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number { - const { salt, difficulty } = challenge; +export function solve(solver: SolverModule, challenge: string): string { + console.log(challenge); + const encoder = new TextEncoder(); + const challenge_buf = encoder.encode(challenge); - const salt_bytes = encoder.encode(salt); - - const salt_ptr = solver.exports.malloc(salt_bytes.length); - if (salt_ptr === 0 || salt_ptr === null) { + const challenge_ptr = solver.exports.malloc(challenge_buf.length); + if (challenge_ptr === 0 || challenge_ptr === null) { throw new Error("Failed to allocate memory for challenge string"); } const memory = new Uint8Array(solver.exports.memory.buffer); - memory.set(salt_bytes, salt_ptr); + memory.set(challenge_buf, challenge_ptr); - const ret = solver.exports.solve_leaading_zeroes_challenge( - salt_ptr, - salt_bytes.length, - difficulty, - ); + const ret = solver.exports.solve(challenge_ptr, challenge_buf.length); - if (ret < 0) { + console.log("RET", ret); + + if (ret <= 0) { throw new Error("Failed to solve challenge"); } - return ret; + 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; } - -export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number { - const { salt, target } = challenge; - const encoder = new TextEncoder(); - - const salt_bytes = encoder.encode(salt); - const target_bytes = encoder.encode(target); - - const salt_ptr = solver.exports.malloc(salt_bytes.length); - if (salt_ptr === 0 || salt_ptr === null) { - throw new Error("Failed to allocate memory for salt string"); - } - - const target_ptr = solver.exports.malloc(target_bytes.length); - if (target_ptr === 0 || target_ptr === null) { - throw new Error("Failed to allocate memory for target string"); - } - - const memory = new Uint8Array(solver.exports.memory.buffer); - memory.set(salt_bytes, salt_ptr); - memory.set(target_bytes, target_ptr); - - const ret = solver.exports.solve_target_number_challenge( - target_ptr, - target_bytes.length, - salt_ptr, - salt_bytes.length, - ); - - if (ret < 0) { - throw new Error("Failed to solve challenge"); - } - - return ret; -} \ No newline at end of file diff --git a/packages/lib/src/validator.ts b/packages/lib/src/validator.ts index 3c5185c..3498735 100644 --- a/packages/lib/src/validator.ts +++ b/packages/lib/src/validator.ts @@ -1,12 +1,11 @@ -import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.'; +import { ChallengeStrategy, type Challenge, type InnerChallenge } 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_leading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number, difficulty: number) => number; - "validate_target_number_challenge": (target_ptr: number, target_len: number, nonce_ptr: number, nonce_len: number, salt_ptr: number, salt_len: number) => number; - "hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint; + "validate": (challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number) => boolean; "memory": WebAssembly.Memory; } @@ -14,159 +13,92 @@ export interface ValidatorModule extends WebAssembly.Instance { exports: WasmExports; } -function array_to_base64(buffer: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buffer))); -} - -async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise { - let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&"); - let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`; - - let challenge: Challenge = { - algorithm: ChallengeAlgorithm.Argon2id, - strategy: ChallengeStrategy.LeadingZeroes, - salt, - difficulty, - }; - - return challenge; -} - -async function generate_target_number_challenge(parameters: Object, max_number: number): Promise { - // in target number config, since we need to generate a target hash, we - // need to hash the salt + nonce, so the client knows what the target is - const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule; - - let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&"); - let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`; - let random_number = new DataView(crypto.getRandomValues(new Uint8Array(4)).buffer).getUint32(0, true) % max_number; - - const encoder = new TextEncoder(); - const salt_bytes = encoder.encode(salt); - const random_number_bytes = encoder.encode(random_number.toString()); - - const salt_ptr = validator.exports.malloc(salt_bytes.length); - const random_number_ptr = validator.exports.malloc(random_number_bytes.length); - - if (salt_ptr === 0 || salt_ptr === null || random_number_ptr === 0 || random_number_ptr === null) { - console.error("Failed to allocate memory for challenge string"); - return null; +function array_to_base64(buffer: ArrayBufferLike): string { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (var i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); } - - const memory = new Uint8Array(validator.exports.memory.buffer); - memory.set(salt_bytes, salt_ptr); - memory.set(random_number_bytes, random_number_ptr); - - let target_blob: bigint = validator.exports.hash(salt_ptr, salt_bytes.length, random_number_ptr, random_number_bytes.length); - let target_ptr = Number(target_blob & BigInt(0xFFFFFFFF)); - let target_len = Number(target_blob >> BigInt(32)); - - validator.exports.free(salt_ptr, salt_bytes.length); - validator.exports.free(random_number_ptr, random_number_bytes.length); - - // do NOT use `memory` here, by this time it has almost definitely been resized and will cause errors to touch - let target_slice = new Uint8Array(validator.exports.memory.buffer.slice(target_ptr, target_ptr + target_len)); - const target = new TextDecoder().decode(target_slice); - - let challenge: Challenge = { - algorithm: ChallengeAlgorithm.Argon2id, - strategy: ChallengeStrategy.TargetNumber, - salt, - target - }; - - return challenge; + return btoa(binary); } -export interface LeadingZeroesChallengeConfig { +export interface kCTFChallengeConfig { parameters: Object; - strategy: ChallengeStrategy.LeadingZeroes; + strategy: ChallengeStrategy.kCTF; difficulty: number; } -export interface TargetNumberChallengeConfig { - parameters: Object; - strategy: ChallengeStrategy.TargetNumber; - max_number: number; -} +export type ChallengeConfig = kCTFChallengeConfig; -export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig; +const VERSION = "s"; + +async function encode_challenge(challenge: InnerChallenge, parameters: Object = {}): Promise { + if (challenge.strategy !== ChallengeStrategy.kCTF) { + throw new Error("Unsupported challenge strategy"); + } + + 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}`; +} export async function generate_challenge(config: ChallengeConfig): Promise { - let challenge: Challenge | null = null; - switch (config.strategy) { - case ChallengeStrategy.LeadingZeroes: - challenge = await generate_leading_zeroes_challenge(config.parameters, config.difficulty); - break; - case ChallengeStrategy.TargetNumber: - challenge = await generate_target_number_challenge(config.parameters, config.max_number); - break; + if (config.difficulty < 1) { + throw new Error("Difficulty must be at least 1"); } - if (challenge === null) { - return null; + const challenge: InnerChallenge = { + strategy: ChallengeStrategy.kCTF, + salt: uuidv7obj(), + difficulty: config.difficulty, } - return challenge; + return { + strategy: ChallengeStrategy.kCTF, + challenge: await encode_challenge(challenge), + }; } -export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise { - const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule - +export async function validate_challenge(challenge: Challenge, challenge_solution: string): 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); - let err; - let memory; - let nonce_bytes, nonce_ptr; - let target_bytes, target_ptr; - switch (challenge.strategy) { - case ChallengeStrategy.LeadingZeroes: - target_bytes = encoder.encode(challenge_solution.challenge); - nonce_bytes = encoder.encode(challenge_solution.nonce); + const challenge_ptr = validator.exports.malloc(challenge_buf.length); + const solution_ptr = validator.exports.malloc(solution_buf.length); - target_ptr = validator.exports.malloc(challenge_solution.challenge.length); - nonce_ptr = validator.exports.malloc(challenge_solution.nonce.length); - - if (target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) { - console.error("Failed to allocate memory for challenge string"); - return false; - } - - memory = new Uint8Array(validator.exports.memory.buffer); - memory.set(target_bytes, target_ptr); - memory.set(nonce_bytes, nonce_ptr); - - err = validator.exports.validate_leading_zeroes_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, challenge.difficulty); - - validator.exports.free(target_ptr, target_bytes.length); - validator.exports.free(nonce_ptr, nonce_bytes.length); - break; - case ChallengeStrategy.TargetNumber: - target_bytes = encoder.encode(challenge.target); - const salt_bytes = encoder.encode(challenge.salt); - nonce_bytes = encoder.encode(challenge_solution.nonce); - - const salt_ptr = validator.exports.malloc(salt_bytes.length); - target_ptr = validator.exports.malloc(target_bytes.length); - nonce_ptr = validator.exports.malloc(nonce_bytes.length); - - if (salt_ptr === 0 || salt_ptr === null || target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) { - console.error("Failed to allocate memory for challenge string"); - return false; - } - - memory = new Uint8Array(validator.exports.memory.buffer); - memory.set(salt_bytes, salt_ptr); - memory.set(target_bytes, target_ptr); - memory.set(nonce_bytes, nonce_ptr); - - err = validator.exports.validate_target_number_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, salt_ptr, salt_bytes.length); - - validator.exports.free(salt_ptr, salt_bytes.length); - validator.exports.free(target_ptr, target_bytes.length); - validator.exports.free(nonce_ptr, nonce_bytes.length); - break; + if (challenge_ptr === 0 || challenge_ptr === null || solution_ptr === 0 || solution_ptr === null) { + console.error("Failed to allocate memory for challenge string"); + return false; } - return err === 0; + 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); + + return is_valid; } \ No newline at end of file diff --git a/packages/widget/package-lock.json b/packages/widget/package-lock.json index ff2b26c..76bc129 100644 --- a/packages/widget/package-lock.json +++ b/packages/widget/package-lock.json @@ -1433,7 +1433,6 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2148,7 +2147,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2422,7 +2420,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2461,7 +2458,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/packages/widget/src/pow-captcha.ts b/packages/widget/src/pow-captcha.ts index d441202..6f9c5d9 100644 --- a/packages/widget/src/pow-captcha.ts +++ b/packages/widget/src/pow-captcha.ts @@ -21,6 +21,9 @@ export class PowCaptcha extends LitElement { background-color: var(--impost-widget-background-color, #070408); border-radius: var(--impost-widget-border-radius, 8px); } + .impost-error-icon { + color: var(--impost-widget-error-icon-color, #FF8117); + } .impost-main { display: flex; align-items: center; @@ -86,32 +89,20 @@ export class PowCaptcha extends LitElement { @state() private solution: string = ''; - @state() - private errorMessage: string = ''; - @state() private challengeData: Challenge | null = null; @state() - private solved: boolean = false; - - @state() - private isSolving: boolean = false; + private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved'; @state() private disabled: boolean = true; - @state() - private hashRate: number = 0; - // stores the nonce and solution atomics private sab: SharedArrayBuffer = new SharedArrayBuffer(8); private solverWorkers: Worker[] | null = null; - private solveStartTime: number | null = null; - private hashRateInterval: number | null = null; - override connectedCallback() { super.connectedCallback(); this.fetchChallenge(); @@ -141,10 +132,6 @@ export class PowCaptcha extends LitElement { override disconnectedCallback() { super.disconnectedCallback(); - if (this.hashRateInterval !== null) { - clearInterval(this.hashRateInterval); - this.hashRateInterval = null; - } for (const worker of this.solverWorkers || []) { worker.terminate(); @@ -157,7 +144,6 @@ export class PowCaptcha extends LitElement { } async fetchChallenge() { - this.errorMessage = ''; if (this.challengeData !== null) { return; } @@ -179,14 +165,14 @@ export class PowCaptcha extends LitElement { }) .catch(error => { console.error('Error fetching challenge:', error); - this.errorMessage = 'Failed to fetch challenge. Please try again.'; + console.error('Failed to fetch challenge'); }); } async initWorkers() { this.solverWorkers = []; - const num_workers = navigator.hardwareConcurrency; + const num_workers = 1; for (let i = 0; i < num_workers; i++) { this.solverWorkers.push(new ChallengeWorker()); } @@ -237,7 +223,8 @@ export class PowCaptcha extends LitElement { let timeout: number; const timeoutPromise = new Promise((_, reject) => { timeout = setTimeout(() => { - this.errorMessage = 'Failed to initialize workers in time. Please refresh the page.'; + console.error('Failed to initialize workers in time'); + this.status = 'error'; reject(new Error(`Function timed out after ${timeoutMs}ms`)); }, timeoutMs); }); @@ -272,19 +259,27 @@ export class PowCaptcha extends LitElement { worker.addEventListener('message', message_handler); worker.addEventListener('error', error_handler); + // switch (request.strategy) { + // case ChallengeStrategy.LeadingZeroes: + // worker.postMessage({ + // strategy: ChallengeStrategy.LeadingZeroes, + // salt: request.salt, + // difficulty: request.difficulty, + // } as WorkerRequest); + // break; + // case ChallengeStrategy.TargetNumber: + // worker.postMessage({ + // strategy: ChallengeStrategy.TargetNumber, + // target: request.target, + // salt: request.salt, + // } as WorkerRequest); + // break; + // } switch (request.strategy) { - case ChallengeStrategy.LeadingZeroes: + case ChallengeStrategy.kCTF: worker.postMessage({ - strategy: ChallengeStrategy.LeadingZeroes, - salt: request.salt, - difficulty: request.difficulty, - } as WorkerRequest); - break; - case ChallengeStrategy.TargetNumber: - worker.postMessage({ - strategy: ChallengeStrategy.TargetNumber, - target: request.target, - salt: request.salt, + strategy: ChallengeStrategy.kCTF, + challenge: request.challenge, } as WorkerRequest); break; } @@ -293,7 +288,8 @@ export class PowCaptcha extends LitElement { async solveChallenge() { if (!this.challengeData || this.solverWorkers === null) { - this.errorMessage = 'Captcha is not ready. Please wait or refresh.'; + console.error('solveChallenge called before challenge is ready'); + this.status = 'error'; return; } @@ -301,25 +297,14 @@ export class PowCaptcha extends LitElement { return; } - this.solveStartTime = performance.now(); - this.hashRateInterval = setInterval(async () => { - const nonce = this.getCurrentWorkingNonce(); - - this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000)); - }, 250); - this.dispatchEvent(new CustomEvent('impost:solve', { - detail: { - solution: this.solution, - }, bubbles: true, composed: true, })) console.log(this.challengeData); - this.isSolving = true; - this.errorMessage = ''; + this.status = 'solving'; this.solution = ''; const atomics_view = new Int32Array(this.sab); @@ -328,19 +313,27 @@ export class PowCaptcha extends LitElement { let request: ChallengeSolveRequest; + // switch (this.challengeData.strategy) { + // case ChallengeStrategy.LeadingZeroes: + // request = { + // strategy: ChallengeStrategy.LeadingZeroes, + // salt: this.challengeData.salt, + // difficulty: this.challengeData.difficulty, + // }; + // break; + // case ChallengeStrategy.TargetNumber: + // request = { + // strategy: ChallengeStrategy.TargetNumber, + // target: this.challengeData.target, + // salt: this.challengeData.salt, + // }; + // break; + // } switch (this.challengeData.strategy) { - case ChallengeStrategy.LeadingZeroes: + case ChallengeStrategy.kCTF: request = { - strategy: ChallengeStrategy.LeadingZeroes, - salt: this.challengeData.salt, - difficulty: this.challengeData.difficulty, - }; - break; - case ChallengeStrategy.TargetNumber: - request = { - strategy: ChallengeStrategy.TargetNumber, - target: this.challengeData.target, - salt: this.challengeData.salt, + strategy: ChallengeStrategy.kCTF, + challenge: this.challengeData.challenge, }; break; } @@ -350,7 +343,7 @@ export class PowCaptcha extends LitElement { // blocking, some workers may block on the read, and as soon as they // unblock, they return 0 since the challenge is already solved. // - // We need to do a better job of tracking solvers, so if one worker + // 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) { @@ -361,31 +354,44 @@ export class PowCaptcha extends LitElement { let solution = await Promise.race(worker_promises); if (solution.type === WorkerResponseType.Error) { - this.errorMessage = solution.error; + console.error("Worker error:", solution.error); + this.status = 'error'; return; } if (solution.type !== WorkerResponseType.Solution) { - this.errorMessage = "Something went wrong, please try again later."; + console.error("Worker sent spurious message"); + this.status = 'error'; return; } - this.solution = Atomics.load(atomics_view, 1).toString(); - this.isSolving = false; - this.solved = true; + // TODO: configure if we should fetch or not + try { + await fetch(`${this.challengeUrl}/challenge`, { + method: 'POST', + body: JSON.stringify({ + challenge: this.challengeData.challenge, + solution: solution.solution, + }), + headers: { + 'Content-Type': 'application/json' + } + }) - if (this.hashRateInterval !== null) { - clearInterval(this.hashRateInterval); - this.hashRateInterval = null; + this.status = 'solved'; + + this.dispatchEvent(new CustomEvent('impost:solved', { + detail: { + challenge: this.challengeData.challenge, + solution: solution.solution, + }, + bubbles: true, + composed: true, + })) + } catch { + console.error('Failed to submit solution'); + this.status = 'error'; } - - this.dispatchEvent(new CustomEvent('impost:solved', { - detail: { - solution: this.solution, - }, - bubbles: true, - composed: true, - })) } solvePreventDefault(event: Event) { @@ -400,12 +406,6 @@ export class PowCaptcha extends LitElement { this.challengejson = ''; } - if (this.errorMessage) { - return html` -
${this.errorMessage}
- `; - } - if (this.challengeData === null) { return html`
Loading captcha challenge...
@@ -416,9 +416,9 @@ export class PowCaptcha extends LitElement {
- ${!this.isSolving ? html` - - ` : html` + ${this.status !== 'solving' ? html`${this.status === 'error' ? html`` : html` + + `}` : html` @@ -431,16 +431,10 @@ export class PowCaptcha extends LitElement { `}
- +