Implement kCTF strategy
This implementation is pretty scuffed, but its more exploratory than anything else.
This commit is contained in:
3
packages/lib/package-lock.json
generated
3
packages/lib/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
export type InnerChallenge = InnerChallengekCTF;
|
||||
|
||||
export type Challenge = ChallengekCTF;
|
||||
@@ -3,8 +3,10 @@ import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"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<WebAssembly.Module> {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<string, Function> & {
|
||||
"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<Challenge> {
|
||||
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<Challenge | null> {
|
||||
// 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<string> {
|
||||
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<Challenge | null> {
|
||||
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<boolean> {
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
|
||||
|
||||
export async function validate_challenge(challenge: Challenge, challenge_solution: string): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
4
packages/widget/package-lock.json
generated
4
packages/widget/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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<SolutionMessage>[] = [];
|
||||
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`
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.challengeData === null) {
|
||||
return html`
|
||||
<div class="loading-message">Loading captcha challenge...</div>
|
||||
@@ -416,9 +416,9 @@ export class PowCaptcha extends LitElement {
|
||||
<div class="impost-widget">
|
||||
<div class="impost-main">
|
||||
<div class="impost-checkbox">
|
||||
${!this.isSolving ? html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.solved}>
|
||||
` : html`
|
||||
${this.status !== 'solving' ? html`${this.status === 'error' ? html`<svg class="impost-error-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 14q-.425 0-.712-.288T11 13V6q0-.425.288-.712T12 5t.713.288T13 6v7q0 .425-.288.713T12 14m0 5q-.425 0-.712-.288T11 18t.288-.712T12 17t.713.288T13 18t-.288.713T12 19"/></svg>` : html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||
`}` : html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||
<g stroke="currentColor">
|
||||
@@ -431,16 +431,10 @@ export class PowCaptcha extends LitElement {
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<label for="impost-checkbox-${this.uid}">${this.solved ? 'Verified' : html`${this.isSolving ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
<label for="impost-checkbox-${this.uid}">${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
</div>
|
||||
|
||||
<div class="impost-footer">
|
||||
${this.isSolving && this.hashRate > 0 ? html`
|
||||
<div>
|
||||
<span>H/s:</span>
|
||||
<span>${this.hashRate.toFixed(2)}</span>
|
||||
</div>
|
||||
` : ``}
|
||||
<div id="provider-link">
|
||||
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
WorkerResponseType,
|
||||
} from "./types/worker";
|
||||
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
|
||||
import { type SolverModule, init_solver, solve } from '@impost/lib/solver';
|
||||
|
||||
let solver: SolverModule | null = null;
|
||||
|
||||
@@ -29,6 +27,7 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
__log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(solver!.exports.memory.buffer, str_ptr, str_len))),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
postMessage({
|
||||
@@ -60,32 +59,19 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_nonce === null || atomic_solution === null) {
|
||||
throw new Error("Atomics not initialized");
|
||||
}
|
||||
|
||||
const { strategy } = event.data;
|
||||
|
||||
let solution: number;
|
||||
switch (strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
solution = solve_leaading_zeroes_challenge(solver, event.data)
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
solution = solve_target_number_challenge(solver, event.data)
|
||||
break;
|
||||
}
|
||||
|
||||
// we are just assuming that if its less than -1, its the min i32
|
||||
if (solution < 0) {
|
||||
return postMessage({
|
||||
let solution: string;
|
||||
try {
|
||||
solution = solve(solver, event.data.challenge);
|
||||
} catch (error: any) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "failed to solve challenge",
|
||||
error: `Failed to solve challenge: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Solution,
|
||||
nonce: solution === -1 ? null : solution.toString()
|
||||
solution,
|
||||
} as SolutionMessage);
|
||||
};
|
||||
|
||||
@@ -12,28 +12,37 @@ interface WorkerInitRequest {
|
||||
sab: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
interface ChallengeLeadingZeroesSolveRequest {
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string;
|
||||
difficulty: number;
|
||||
// interface ChallengeLeadingZeroesSolveRequest {
|
||||
// strategy: ChallengeStrategy.LeadingZeroes;
|
||||
// salt: string;
|
||||
// difficulty: number;
|
||||
// }
|
||||
|
||||
// interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||
// type: WorkerMessageType.Challenge;
|
||||
// }
|
||||
|
||||
// interface ChallengeTargetNumberSolveRequest {
|
||||
// strategy: ChallengeStrategy.TargetNumber;
|
||||
// target: string;
|
||||
// salt: string;
|
||||
// }
|
||||
|
||||
// interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||
// type: WorkerMessageType.Challenge;
|
||||
// }
|
||||
|
||||
interface ChallengekCTFSolveRequest {
|
||||
strategy: ChallengeStrategy.kCTF;
|
||||
challenge: string;
|
||||
}
|
||||
|
||||
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||
interface WorkerChallengekCTFSolveRequest extends ChallengekCTFSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
interface ChallengeTargetNumberSolveRequest {
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
target: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
export type ChallengeSolveRequest = ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest;
|
||||
type WorkerChallengeSolveRequest = WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest;
|
||||
export type ChallengeSolveRequest = ChallengekCTFSolveRequest;
|
||||
type WorkerChallengeSolveRequest = WorkerChallengekCTFSolveRequest;
|
||||
|
||||
export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
|
||||
|
||||
@@ -50,7 +59,7 @@ interface ErrorMessageResponse {
|
||||
|
||||
interface SolutionMessageResponse {
|
||||
type: WorkerResponseType.Solution;
|
||||
nonce: string;
|
||||
solution: string;
|
||||
}
|
||||
|
||||
interface InitOkMessageResponse {
|
||||
|
||||
Reference in New Issue
Block a user