3 Commits

Author SHA1 Message Date
Zoe
d0f4936b84 further benchmarking stuff 2025-11-28 14:53:06 -06:00
Zoe
e16383e9b9 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.
2025-11-25 18:09:17 +00:00
Zoe
570531fe32 Implement kCTF strategy
This implementation is pretty scuffed, but its more exploratory than anything else.
2025-11-21 16:20:07 +00:00
38 changed files with 1754 additions and 1183 deletions

View File

@@ -3,9 +3,9 @@
Impost /ˈimˌpōst/ _noun_ a tax or compulsory payment 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 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 spying on your users and using heavy, bloated captchas, Impost uses PoW to
on sending requests. To a single user, this is a negligable few seconds, but at impose a cost on sending requests. To a single user, this is a negligable few
scale, it can be a significant deterrent to spam. seconds, but at scale, it can be a significant deterrent to spam.
This is the impost monorepo, containing the following packages: 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, 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 `@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.

View File

@@ -0,0 +1,187 @@
<script setup lang="ts">
let { data: challengeData } = await useFetch('/api/pow/challenge');
let { data: powData } = await useFetch('/api/pow');
if (!challengeData || !powData) {
throw createError({
statusCode: 500,
message: 'Failed to fetch data',
});
}
const algorithms = {
"argon2": {
name: 'argon2',
label: 'Argon2',
strategies: ['leading_zeroes', 'target_number'],
},
"kctf": {
name: 'kctf',
label: 'kCTF',
strategies: ['null'],
},
"sha256": {
name: 'sha256',
label: 'SHA256',
strategies: ['leading_zeroes', 'target_number'],
},
};
async function refresh() {
challengeData.value = await $fetch('/api/pow/challenge');
powData.value = await $fetch('/api/pow');
resetCaptcha();
}
function resetCaptcha() {
document.querySelector("pow-captcha")!.dispatchEvent(new CustomEvent('reset', {
detail: {
challenge: challengeData.value.challenge,
}
}));
}
let bench_results = ref([])
let start = ref(0);
let continue_bench: Promise<void> | null = null;
async function bench() {
for (let algorithm_name in algorithms) {
let algorithm = algorithms[algorithm_name as "sha256" | "argon2" | "kctf"];
for (let strategy of algorithm.strategies) {
for (let i = 1; i <= 3; i++) {
// reduce statistical anomolies by running tests multiple times and averaging after tha fact
for (let j = 0; j < 5; j++) {
let difficulty;
switch (strategy) {
case 'leading_zeroes':
difficulty = i;
break;
case 'null':
case 'target_number':
// these tests scale linearly, so to try to match the
// complexity of leading_zeroes, we grow the difficulty
// exponentially
difficulty = Math.pow(16, i);
break;
}
await changeAlgorithm(algorithm_name);
if (strategy !== 'null') {
await changeStrategy(strategy);
}
await $fetch('/api/pow/difficulty', {
method: 'PUT',
body: JSON.stringify({
difficulty: difficulty,
}),
})
// sleep for 300ms
await new Promise((resolve) => setTimeout(resolve, 750));
await refresh();
continue_bench = new Promise((resolve) => {
document.querySelector("pow-captcha")!.addEventListener('impost:solved', () => {
resolve();
});
});
start.value = performance.now();
document.querySelector("pow-captcha")!.dispatchEvent(new Event('solve'));
await continue_bench;
let end = performance.now();
const data = {
algorithm: algorithm_name,
cores: navigator.hardwareConcurrency,
strategy: strategy,
difficulty: difficulty,
time: end - start.value,
};
const should_scroll = document.documentElement.scrollTop + document.documentElement.clientHeight >= document.documentElement.scrollHeight;
bench_results.value.push(data);
if (should_scroll) {
document.documentElement.scrollTop = document.documentElement.scrollHeight;
}
}
}
}
}
}
function solved(ev: CustomEvent) {
console.log("Solved:", ev.detail.solution);
}
async function changeAlgorithmEV(ev: Event) {
changeAlgorithm(ev.target.value);
refresh();
}
async function changeAlgorithm(algorithm: string) {
await $fetch('/api/pow/algorithm', {
method: 'PUT',
body: JSON.stringify({
algorithm: algorithm
}),
});
}
async function changeStrategyEV(ev: Event) {
changeStrategy(ev.target.value);
refresh();
}
async function changeStrategy(strategy: string) {
await $fetch('/api/pow/strategy', {
method: 'PUT',
body: JSON.stringify({
strategy: strategy
}),
});
}
</script>
<template>
<pow-captcha challengeUrl="/api/pow" :challengejson="JSON.stringify(challengeData!.challenge)"
@impost:solved="solved" />
<div class="flex flex-row gap-4">
<div class="flex flex-row gap-2" v-for="algorithm in algorithms" :key="algorithm.label">
<input type="radio" name="algorithm" @change="changeAlgorithmEV" :value="algorithm.name"
:id="algorithm.name" :checked="powData!.algorithm === algorithm.name"> <label :for="algorithm.name">{{
algorithm.label
}}</label>
</div>
</div>
<div v-if="algorithms[powData!.algorithm].strategies.length > 1 && powData!.algorithm === algorithms[powData!.algorithm].name"
class="flex flex-row gap-4">
<div class="flex flex-row gap-2" v-for="strategy in algorithms[powData!.algorithm].strategies">
<input type="radio" name="strategy" @change="changeStrategyEV" :value="strategy" :id="strategy"
:checked="powData!.strategy === strategy"> <label :for="strategy">{{ strategy }}</label>
</div>
</div>
<input type="button" value="Start benchmark" @click="bench" />
<div v-if="bench_results.length > 0">
<table>
<thead>
<tr>
<th>Algorithm</th>
<th>Strategy</th>
<th>Difficulty</th>
<th>Time (ms)</th>
</tr>
</thead>
<tbody>
<tr v-for="result in bench_results" :key="result.algorithm + result.strategy + result.difficulty">
<td>{{ algorithms[result.algorithm].label }}</td>
<td>{{ result.strategy }}</td>
<td>{{ result.difficulty }}</td>
<td>{{ result.time }}</td>
</tr>
</tbody>
</table>
</div>
</template>

View File

@@ -1,411 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { type WorkerRequest, type SolutionMessage, WorkerResponseType, WorkerMessageType, ChallengeStrategy, type Challenge } from '~/types/pow'; const { data: challengeData } = await useFetch('/api/pow/challenge');
import WASMSolverUrl from "~/utils/solver.wasm?url";
import ChallengeWorker from '~/utils/worker?worker';
let shared_atomics = new SharedArrayBuffer(12); if (!challengeData) {
throw createError({
let workers: Worker[] = []; statusCode: 500,
let workers_initialized = false; message: 'Failed to fetch challenge',
if (import.meta.client) {
try {
// const num_workers = 1;
const num_workers = navigator.hardwareConcurrency;
for (let i = 0; i < num_workers; i++) {
workers.push(new ChallengeWorker());
}
} catch (error: any) {
console.error("Failed to create worker:", error);
}
}
let autoSolve: Ref<boolean> = ref(false);
watch(autoSolve, async () => {
if (autoSolve.value) {
while (autoSolve.value) {
if (solving.value) {
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
await getChallenge();
await solveChallenge();
if (!autoSolve.value) {
break;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
});
let total_solved: Ref<number> = ref(0);
let total_solving_for: Ref<number> = ref(0);
function pluralize(value: number, string: string) {
return value === 1 ? string : `${string}s`;
}
let challenge_loading: Ref<boolean> = ref(false);
let challenge_loading_indicator: Ref<string> = ref('');
let challenge: Ref<Challenge | null> = ref(null);
let nonce: Ref<string | null> = ref(null);
let solved: Ref<boolean> = ref(false);
let solving_for: Ref<string> = ref('0');
const number_formatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short',
maximumFractionDigits: 2,
});
// the hashrate of all the runs
let hashrate_array: Ref<Array<number>> = ref([]);
let hashrate = computed(() => {
if (hashrate_array.value.length === 0) {
return 0;
}
return hashrate_array.value.reduce((a, b) => a + b, 0) / hashrate_array.value.length;
});
let solving = ref(false);
let solveTime: Ref<number> = ref(0);
let solveTimeout: Ref<any | null> = ref(null);
let challenge_error: Ref<string | null> = ref(null);
let difficulty: Ref<number> = ref(0);
let { data } = await useFetch('/api/pow/difficulty');
if (data.value) {
difficulty.value = data.value.difficulty;
}
const MESSAGE_TIMEOUT = 3000;
async function getChallenge() {
challenge_error.value = null;
challenge_loading_indicator.value = '';
challenge.value = null;
nonce.value = null;
challenge_loading.value = true;
const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let spinner_index = 0;
const loading_interval = setInterval(() => {
challenge_loading_indicator.value = spinners[spinner_index]!;
spinner_index = (spinner_index + 1) % spinners.length;
console.log(spinners[spinner_index]);
}, 100);
try {
const new_challenge = await $fetch('/api/pow/challenge');
challenge.value = new_challenge.challenge;
} catch (error: any) {
console.error("Failed to get challenge:", error);
challenge_error.value = `Failed to get challenge: ${error.message}`;
} finally {
challenge_loading.value = false;
clearInterval(loading_interval);
}
}
async function initWorkers() {
if (workers_initialized) {
throw createError("Workers already initialized");
}
workers_initialized = true;
const module = await WebAssembly.compileStreaming(fetch(WASMSolverUrl));
const atomics_view = new Int32Array(shared_atomics);
Atomics.store(atomics_view, 0, 0);
Atomics.store(atomics_view, 1, 0);
console.debug(`Initializing ${workers.length} workers`);
let worker_promises: Promise<void>[] = [];
for (let i = 0; i < workers.length; i++) {
const worker = workers[i]!;
worker_promises.push(new Promise<void>((resolve, reject) => {
const message_handler = (event: MessageEvent<SolutionMessage>) => {
if (event.data.type === WorkerResponseType.Error) {
console.error("Worker error:", event.data.error);
reject(event.data.error);
}
if (event.data.type === WorkerResponseType.Ok) {
resolve();
}
reject(new Error("Unexpected message from worker"));
};
const error_handler = (error: ErrorEvent) => {
console.error("Worker error:", error);
reject(error);
};
worker.addEventListener('message', message_handler);
worker.addEventListener('error', error_handler);
worker.postMessage({
type: WorkerMessageType.Init,
module: module,
sab: shared_atomics,
} as WorkerRequest);
}));
}
const timeoutMs = 10 * 1000;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Function timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
await Promise.race([
Promise.all(worker_promises),
timeoutPromise,
]);
console.log("All workers initialized");
}
async function getChallengeSolution(worker: Worker, request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string }): Promise<SolutionMessage> {
return new Promise<SolutionMessage>((resolve, reject) => {
const message_handler = (event: MessageEvent<SolutionMessage>) => {
worker.removeEventListener('message', message_handler);
worker.removeEventListener('error', error_handler);
resolve(event.data);
};
const error_handler = (error: ErrorEvent) => {
worker.removeEventListener('error', error_handler);
worker.removeEventListener('message', message_handler);
console.error("Worker error:", error);
reject(error);
};
worker.addEventListener('message', message_handler);
worker.addEventListener('error', error_handler);
switch (request.strategy) {
case ChallengeStrategy.LeadingZeroes:
worker.postMessage({
strategy: ChallengeStrategy.LeadingZeroes,
target: request.target,
difficulty: request.difficulty,
} as WorkerRequest);
break;
case ChallengeStrategy.TargetNumber:
worker.postMessage({
strategy: ChallengeStrategy.TargetNumber,
target: request.target,
salt: request.salt,
} as WorkerRequest);
break;
}
}); });
} }
async function solveChallenge() { function solved(ev: CustomEvent) {
if (!challenge.value?.target) { console.log("Solved:", ev.detail.solution);
return; // $fetch('/api/pow/challenge', {
} // method: 'POST',
// body: JSON.stringify({
if (!workers_initialized) { // challenge: ev.detail.challenge,
try { // solution: ev.detail.solution,
await initWorkers(); // }),
} catch (error: any) { // });
console.error("Failed to initialize workers:", error);
return;
}
}
const atomics_view = new Int32Array(shared_atomics);
Atomics.store(atomics_view, 0, 0);
Atomics.store(atomics_view, 1, -1);
solved.value = false;
challenge_error.value = null;
solving.value = true;
solveTime.value = 0;
solving_for.value = '0';
let startTime = performance.now();
let solving_for_interval = setInterval(() => {
solving_for.value = ((performance.now() - startTime) / 1000).toFixed(1);
}, 100);
function cleanup() {
clearTimeout(solveTimeout.value);
solveTimeout.value = setTimeout(() => {
solveTime.value = 0;
}, MESSAGE_TIMEOUT);
solved.value = false;
solving.value = false;
clearInterval(solving_for_interval);
}
try {
let request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string };
switch (challenge.value.strategy) {
case ChallengeStrategy.LeadingZeroes:
request = {
strategy: ChallengeStrategy.LeadingZeroes,
target: challenge.value.target,
difficulty: challenge.value.difficulty,
};
break;
case ChallengeStrategy.TargetNumber:
request = {
strategy: ChallengeStrategy.TargetNumber,
target: challenge.value.target,
salt: challenge.value.salt,
};
break;
}
let worker_promises: Promise<SolutionMessage>[] = [];
for (let worker of workers) {
// dispatch to all workers, func is async so it will not block
worker_promises.push(getChallengeSolution(worker, request));
}
let solution = await Promise.race(worker_promises);
if (solution.type === WorkerResponseType.Error) {
throw createError(solution.error);
}
if (solution.type === WorkerResponseType.Ok) {
throw createError("spurious solution");
}
console.log(shared_atomics.slice(8, 12));
nonce.value = Atomics.load(atomics_view, 1).toString();
solveTime.value = Math.floor(performance.now() - startTime);
total_solved.value += 1;
total_solving_for.value += solveTime.value;
// since nonce is the number of iterations we have completed, we can divide that by how long it took in second
// to get H/s
hashrate_array.value.push(+Atomics.load(atomics_view, 1) / (solveTime.value / 1000));
clearTimeout(solveTimeout.value);
await $fetch('/api/pow/challenge', {
method: 'POST',
body: {
challenge: challenge.value.target,
nonce: nonce.value,
}
});
solved.value = true;
solveTimeout.value = setTimeout(() => {
solveTime.value = 0;
solved.value = false;
}, MESSAGE_TIMEOUT);
switch (challenge.value.strategy) {
case ChallengeStrategy.LeadingZeroes:
console.debug("Solved challenge with difficulty", challenge.value.difficulty, "in " + solveTime.value + "ms");
break;
case ChallengeStrategy.TargetNumber:
console.debug("Solved challenge with salt", challenge.value.salt, "in " + solveTime.value + "ms");
break;
}
solving.value = false;
clearInterval(solving_for_interval);
} catch (error: any) {
challenge_error.value = `Failed to solve challenge: ${error.message}`;
console.error(error);
cleanup();
}
} }
async function setDifficulty(difficulty: number) { function formsubmit(ev: Event) {
const response = await $fetch('/api/pow/difficulty', { console.log("Submitted form");
method: 'PUT',
body: {
difficulty,
}
});
console.log(response);
} }
</script> </script>
<template> <template>
<div class="flex justify-between" v-if="hashrate_array.length !== 0"> <div class="flex justify-center items-center h-screen w-screen ">
<span>Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s</span> <form @submit.prevent="formsubmit"
<span>You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
{{ number_formatter.format(total_solving_for / 1000) }}s</span> <pow-captcha challengeUrl="/api/pow" auto="onsubmit"
<span>Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s</span> :challengejson="JSON.stringify(challengeData!.challenge)" @impost:solved="solved" />
</div> <input class="bg-blue-7 text-white font-semibold px-4 py-2.5 border-0 rounded-md" type="submit"
<p v-else>You have not solved any challenges yet</p> value="Submit" />
<p>Challenge: <span v-if="challenge_loading">{{ challenge_loading_indicator }}</span> </form>
<span v-else>{{ challenge }}</span>
</p>
<p>Nonce: {{ nonce }}</p>
<button @click="getChallenge()" :disabled="solving || challenge_loading">
Get Challenge
</button>
<button @click="solveChallenge()" :disabled="solving || challenge === null || nonce !== null">
<span v-if="!solving">Solve Challenge</span>
<span v-else>Solving challenge for {{ solving_for }}s...</span>
</button>
<div>
<div v-if="solveTime && !challenge_error" class="min-h-[1rem]">
<span v-if="solved">Challenge solved in {{ solveTime }}ms!</span>
<span v-else>Validating solution...</span>
</div>
<div v-else-if="challenge_error">{{ challenge_error }}</div>
<p v-else class="empty-p"><!-- Empty so there is no content shift when there is text or isnt --></p>
</div>
<div class="flex flex-row w-fit">
<label for="autoSolve">Auto solve</label>
<input class="w-min" type="checkbox" v-model="autoSolve" id="autoSolve"></input>
</div>
<div class="flex flex-col w-fit">
<label for="difficulty">Difficulty</label>
<input class="w-min" type="number" min="1" max="64" v-model="difficulty" @change="setDifficulty(difficulty)"
id="difficulty"></input>
</div> </div>
</template> </template>
<style scoped>
button {
min-width: 140px;
padding: 0.25rem 0.375rem;
}
.empty-p {
margin: 0;
&::after {
content: "-";
visibility: hidden;
}
}
</style>

View File

@@ -1,38 +0,0 @@
<script setup lang="ts">
const { data: challengeData } = await useFetch('/api/pow/challenge');
if (!challengeData) {
throw createError({
statusCode: 500,
message: 'Failed to fetch challenge',
});
}
function solved(ev: CustomEvent) {
console.log("Solved:", ev.detail.solution);
}
function formsubmit(ev: Event) {
console.log("Submitted form");
}
onMounted(() => {
document.addEventListener('impost:solved', (ev) => solved(ev as CustomEvent));
});
onUnmounted(() => {
document.removeEventListener('impost:solved', (ev) => solved(ev as CustomEvent));
});
</script>
<template>
<div class="flex justify-center items-center h-screen w-screen ">
<form @submit.prevent="formsubmit"
class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
<pow-captcha challengeUrl="/api/pow" auto="onsubmit"
:challengejson="JSON.stringify(challengeData!.challenge)" @impost:solved="solved" />
<input class="bg-blue-7 text-white font-semibold px-4 py-2.5 border-0 rounded-md" type="submit"
value="Submit" />
</form>
</div>
</template>

View File

@@ -1,6 +0,0 @@
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
export function getWorkerName() {
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}`;
}

View File

@@ -1,170 +0,0 @@
// This worker just sits on another thread and waits for message to solve
// challenges so that we dont block the render thread
import {
type WorkerRequest,
type SolutionMessage,
WorkerMessageType,
WorkerResponseType,
ChallengeStrategy,
} from "~/types/pow";
const worker_name = getWorkerName();
let solver: SolverModule | null = null;
let atomic_nonce: Int32Array | null = null;
let atomic_solution: Int32Array | null = null;
async function loadWasmSolver(module: WebAssembly.Module) {
if (atomic_nonce === null || atomic_solution === null) {
throw createError("Atomics not initialized");
}
console.debug(`[${worker_name}]: Loading WASM solver`);
solver = await WebAssembly.instantiate(module, {
env: {
__get_solution: () => Atomics.load(atomic_solution!, 0),
__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: (ptr: number, len: number) => {
const string_data = new Uint8Array(solver!.exports.memory.buffer, ptr, len);
console.log(`[${worker_name}]: ${new TextDecoder().decode(string_data)}`);
},
}
}) as unknown as SolverModule;
console.debug(`[${worker_name}]: WASM solver loaded`);
}
onmessage = async (event: MessageEvent<WorkerRequest>) => {
if (event.data.type === WorkerMessageType.Init) {
console.log(`[${worker_name}]: Initializing...`);
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
atomic_solution = new Int32Array(event.data.sab, 4, 1);
try {
await loadWasmSolver(event.data.module);
} catch (error: any) {
console.error(`[${worker_name}]: Failed to load WASM solver:`, error);
postMessage({
type: WorkerResponseType.Error,
error: `Could not load WASM solver: ${error.message}`,
} as SolutionMessage);
return;
}
if (!solver) {
console.error(`[${worker_name}]: Failed to load WASM solver`);
postMessage({
type: WorkerResponseType.Error,
error: "Failed to load WASM solver",
} as SolutionMessage);
return;
}
postMessage({
type: WorkerResponseType.Ok,
} as SolutionMessage);
return;
}
if (!solver) {
postMessage({
type: WorkerResponseType.Error,
error: "WASM solver not loaded",
} as SolutionMessage);
return;
}
const { strategy } = event.data;
const encoder = new TextEncoder();
let solution: number;
let target: string = event.data.target;
let target_bytes, target_ptr;
let memory;
switch (strategy) {
case ChallengeStrategy.LeadingZeroes:
const { difficulty } = event.data;
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, difficulty: ${difficulty}`);
target_bytes = encoder.encode(target);
target_ptr = solver.exports.malloc(target_bytes.length);
if (target_ptr === 0 || target_ptr === null) {
console.error(`[${worker_name}]: Failed to allocate memory for challenge string`);
postMessage({
type: WorkerResponseType.Error,
error: "Failed to allocate memory for challenge string",
} as SolutionMessage);
return;
}
memory = new Uint8Array(solver.exports.memory.buffer);
memory.set(target_bytes, target_ptr);
solution = solver.exports.solve_leaading_zeroes_challenge(
target_ptr,
target.length,
difficulty,
);
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
break;
case ChallengeStrategy.TargetNumber:
const { salt } = event.data;
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, salt: ${salt}`);
const salt_bytes = encoder.encode(salt);
target_bytes = encoder.encode(target);
const salt_ptr = solver.exports.malloc(salt_bytes.length);
if (salt_ptr === 0 || salt_ptr === null) {
console.error(`[${worker_name}]: Failed to allocate memory for salt string`);
postMessage({
type: WorkerResponseType.Error,
error: "Failed to allocate memory for salt string",
} as SolutionMessage);
return;
}
target_ptr = solver.exports.malloc(target_bytes.length);
if (target_ptr === 0 || target_ptr === null) {
console.error(`[${worker_name}]: Failed to allocate memory for target string`);
postMessage({
type: WorkerResponseType.Error,
error: "Failed to allocate memory for target string",
} as SolutionMessage);
return;
}
memory = new Uint8Array(solver.exports.memory.buffer);
memory.set(salt_bytes, salt_ptr);
memory.set(target_bytes, target_ptr);
solution = solver.exports.solve_target_number_challenge(
target_ptr,
target_bytes.length,
salt_ptr,
salt_bytes.length,
);
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
break;
}
// we are just assuming that if its less than -1, its the min i32
if (solution < 0) {
return postMessage({
type: WorkerResponseType.Error,
error: "failed to solve challenge",
} as SolutionMessage);
}
postMessage({
type: WorkerResponseType.Solution,
nonce: solution === -1 ? null : solution.toString()
} as SolutionMessage);
};

View File

@@ -1,3 +1,4 @@
algorithm = "argon2"
strategy = "target_number" strategy = "target_number"
[leading_zeroes] [leading_zeroes]
@@ -5,3 +6,6 @@ difficulty = 4
[target_number] [target_number]
max_number = 10000 max_number = 10000
[kctf]
difficulty = 100

View File

@@ -6,8 +6,8 @@
"": { "": {
"name": "hello-nuxt", "name": "hello-nuxt",
"dependencies": { "dependencies": {
"@impost/lib": "file:../packages/lib", "@impost/lib": "^0.1.0",
"@impost/widget": "file:../packages/widget", "@impost/widget": "^0.1.0",
"@lit/reactive-element": "^2.1.1", "@lit/reactive-element": "^2.1.1",
"js-toml": "^1.0.2", "js-toml": "^1.0.2",
"lit-element": "^4.2.1", "lit-element": "^4.2.1",
@@ -41,6 +41,9 @@
"name": "@impost/lib", "name": "@impost/lib",
"version": "0.1.0", "version": "0.1.0",
"license": "BSL-1.0", "license": "BSL-1.0",
"dependencies": {
"uuidv7": "^1.0.2"
},
"devDependencies": { "devDependencies": {
"oxc-minify": "^0.97.0", "oxc-minify": "^0.97.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",

View File

@@ -16,8 +16,8 @@
"nuxt": "latest", "nuxt": "latest",
"nuxt-ssr-lit": "1.6.32", "nuxt-ssr-lit": "1.6.32",
"zod": "^4.1.12", "zod": "^4.1.12",
"@impost/lib": "file:../packages/lib", "@impost/lib": "^0.1.0",
"@impost/widget": "file:../packages/widget" "@impost/widget": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.0", "@types/node": "^24.10.0",

View File

@@ -0,0 +1,7 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
return {
algorithm: config.algorithm
}
})

View File

@@ -0,0 +1,43 @@
import { defineEventHandler } from 'h3'
import { ChallengeAlgorithm } from '@impost/lib';
import * as z from 'zod';
const algorithmSchema = z.object({
algorithm: z.enum(ChallengeAlgorithm),
});
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, algorithmSchema.safeParse);
if (!body.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation failed'
})
}
switch (body.data.algorithm) {
case 'sha256':
case 'argon2':
config.algorithm = body.data.algorithm;
config.strategy = config.strategy || 'leading_zeroes';
switch (config.strategy) {
case 'leading_zeroes':
config.leading_zeroes.difficulty = config.leading_zeroes.difficulty || 4;
break;
case 'target_number':
config.target_number.max_number = config.target_number.max_number || 10_000;
break;
}
break;
case 'kctf':
config.algorithm = body.data.algorithm;
config.kctf = config.kctf || {};
config.kctf.difficulty = config.kctf.difficulty || 100;
break;
}
return {
message: `Algorithm set to ${config.algorithm}`
};
});

View File

@@ -1,25 +1,58 @@
import { defineEventHandler } from 'h3' import { defineEventHandler } from 'h3'
import { config } from '~~/server/utils/config'; import { config } from '~~/server/utils/config';
import { generate_challenge } from '@impost/lib/validator'; import { generate_challenge, kCTFChallengeConfig, Argon2ChallengeConfig, SHA256ChallengeConfig } from '@impost/lib/validator';
import { ChallengeStrategy } from '@impost/lib'; import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow'; import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow';
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
let challenge_config; let challenge_config;
switch (config.algorithm) {
case ChallengeAlgorithm.SHA256:
switch (config.strategy) { switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
challenge_config = { challenge_config = {
parameters: { expires_at: CHALLENGE_TIMEOUT_MS }, algorithm: ChallengeAlgorithm.SHA256,
strategy: config.strategy, strategy: ChallengeStrategy.LeadingZeroes,
difficulty: config.leading_zeroes?.difficulty!, difficulty: config.leading_zeroes.difficulty,
}; parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS },
} as SHA256ChallengeConfig;
break; break;
case ChallengeStrategy.TargetNumber: case ChallengeStrategy.TargetNumber:
challenge_config = { challenge_config = {
parameters: { expires_at: CHALLENGE_TIMEOUT_MS }, algorithm: ChallengeAlgorithm.SHA256,
strategy: config.strategy, strategy: ChallengeStrategy.TargetNumber,
max_number: config.target_number.max_number, difficulty: config.target_number.max_number,
}; parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS },
} as SHA256ChallengeConfig;
break;
}
break;
case ChallengeAlgorithm.Argon2:
switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes:
challenge_config = {
algorithm: ChallengeAlgorithm.Argon2,
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.Argon2,
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 = {
algorithm: ChallengeAlgorithm.kCTF,
difficulty: config.kctf.difficulty,
parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS },
} as kCTFChallengeConfig;
break; break;
} }

View File

@@ -4,8 +4,9 @@ import * as z from 'zod';
import { outstandingChallenges } from '~~/server/utils/pow'; import { outstandingChallenges } from '~~/server/utils/pow';
const challengeSchema = z.object({ const challengeSchema = z.object({
challenge: z.string(), salt: z.string(),
nonce: z.string() // either a string if the algorithm is kCTF, or a number if the algorithm is Argon2 or SHA256
solution: z.string().or(z.number()),
}) })
// post handler that takes in the challenge, and the nonce // post handler that takes in the challenge, and the nonce
@@ -19,19 +20,25 @@ export default defineEventHandler(async (event) => {
}) })
} }
let target = body.data.challenge; let { salt, solution } = body.data;
let nonce = body.data.nonce;
const outstanding_challenge = outstandingChallenges.get(salt);
if (outstanding_challenge === undefined) {
throw createError({
statusCode: 400,
statusMessage: 'Challenge not found'
})
}
// check if the challenge is valid // check if the challenge is valid
let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, { const challenge_valid = await validate_challenge(outstanding_challenge.challenge, solution);
challenge: target,
nonce: nonce console.log("CHALLENGE VALID", challenge_valid);
});
if (challenge_valid) { if (challenge_valid) {
// clear the challenge // clear the challenge
clearTimeout(outstandingChallenges.get(target)!.timeout); clearTimeout(outstandingChallenges.get(salt)!.timeout);
outstandingChallenges.delete(target); outstandingChallenges.delete(salt);
return { return {
message: 'Challenge solved' message: 'Challenge solved'

View File

@@ -1,9 +1,14 @@
import { defineEventHandler } from 'h3' import { defineEventHandler } from 'h3'
import { ChallengeStrategy } from '@impost/lib'; import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
let difficulty: number; let difficulty: number;
console.log("CONFIG", config);
switch (config.algorithm) {
case ChallengeAlgorithm.SHA256:
case ChallengeAlgorithm.Argon2:
switch (config.strategy) { switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
difficulty = config.leading_zeroes.difficulty!; difficulty = config.leading_zeroes.difficulty!;
@@ -12,6 +17,11 @@ export default defineEventHandler((event) => {
difficulty = config.target_number.max_number!; difficulty = config.target_number.max_number!;
break; break;
} }
break;
case ChallengeAlgorithm.kCTF:
difficulty = config.kctf.difficulty!;
break;
}
return { return {
difficulty difficulty

View File

@@ -1,11 +1,14 @@
import { defineEventHandler } from 'h3' import { defineEventHandler } from 'h3'
import { ChallengeStrategy } from '@impost/lib'; import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event) const body = await readBody(event)
let difficulty = body.difficulty; let difficulty = body.difficulty;
switch (config.algorithm) {
case ChallengeAlgorithm.SHA256:
case ChallengeAlgorithm.Argon2:
switch (config.strategy) { switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
if (!difficulty || difficulty < 1 || difficulty > 64) { if (!difficulty || difficulty < 1 || difficulty > 64) {
@@ -28,8 +31,13 @@ export default defineEventHandler(async (event) => {
config.target_number.max_number = difficulty; config.target_number.max_number = difficulty;
break; break;
} }
break;
case ChallengeAlgorithm.kCTF:
config.kctf.difficulty = difficulty;
break;
}
return { return {
message: 'Challenge difficulty set' message: `Challenge difficulty set to ${difficulty}`
}; };
}); });

View File

@@ -0,0 +1,35 @@
import { ChallengeAlgorithm } from '@impost/lib';
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
let difficulty: number;
switch (config.algorithm) {
case ChallengeAlgorithm.SHA256:
case ChallengeAlgorithm.Argon2:
switch (config.strategy) {
case 'leading_zeroes':
difficulty = config.leading_zeroes.difficulty!;
break;
case 'target_number':
difficulty = config.target_number.max_number!;
break;
}
break;
case ChallengeAlgorithm.kCTF:
difficulty = config.kctf.difficulty!;
break;
default:
throw createError({
statusCode: 500,
statusMessage: 'Unknown algorithm',
})
break;
}
return {
difficulty,
algorithm: config.algorithm,
strategy: config.strategy || undefined,
}
})

View File

@@ -0,0 +1,14 @@
import { defineEventHandler } from "h3";
import { ChallengeAlgorithm } from "@impost/lib";
export default defineEventHandler(async (event) => {
if (config.algorithm === ChallengeAlgorithm.kCTF) {
return {
strategy: undefined,
}
}
return {
strategy: config.strategy,
}
});

View File

@@ -0,0 +1,43 @@
import { defineEventHandler } from 'h3'
import { ChallengeAlgorithm, ChallengeStrategy } from '@impost/lib';
import * as z from 'zod';
const strategySchema = z.object({
strategy: z.enum(ChallengeStrategy),
});
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, strategySchema.safeParse);
if (!body.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation failed'
})
}
switch (config.algorithm) {
case ChallengeAlgorithm.SHA256:
case ChallengeAlgorithm.Argon2:
config.strategy = body.data.strategy;
switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes:
config.leading_zeroes = config.leading_zeroes || {};
config.leading_zeroes.difficulty = config.leading_zeroes.difficulty || 4;
break;
case ChallengeStrategy.TargetNumber:
config.target_number = config.target_number || {};
config.target_number.max_number = config.target_number.max_number || 10_000;
break;
}
break;
case ChallengeAlgorithm.kCTF:
return {
message: "Strategy cannot be set for kCTF"
}
}
return {
message: `Strategy set to ${config.strategy}`
};
});

View File

@@ -1,30 +1,53 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { load } from 'js-toml'; import { load } from 'js-toml';
import z from 'zod'; import z from 'zod';
import { ChallengeStrategy } from "@impost/lib"; import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib";
const LeadingZeroesSchema = z.object({ const SHA256Schema = z.discriminatedUnion("strategy", [
z.object({
algorithm: z.literal(ChallengeAlgorithm.SHA256),
strategy: z.literal(ChallengeStrategy.LeadingZeroes), strategy: z.literal(ChallengeStrategy.LeadingZeroes),
leading_zeroes: z.object({ leading_zeroes: z.object({
difficulty: z.number().int().min(1).max(64), difficulty: z.number().int().min(1).max(64),
}), }),
}); }),
z.object({
const TargetNumberSchema = z.object({ algorithm: z.literal(ChallengeAlgorithm.SHA256),
strategy: z.literal(ChallengeStrategy.TargetNumber), strategy: z.literal(ChallengeStrategy.TargetNumber),
target_number: z.object({ target_number: z.object({
max_number: z.number().int().min(1).max(100_000), max_number: z.number().int().min(1).max(100_000),
}), }),
}),
]);
const Argon2Schema = z.discriminatedUnion("strategy", [
z.object({
algorithm: z.literal(ChallengeAlgorithm.Argon2),
strategy: z.literal(ChallengeStrategy.LeadingZeroes),
leading_zeroes: z.object({
difficulty: z.number().int().min(1).max(64),
}),
}),
z.object({
algorithm: z.literal(ChallengeAlgorithm.Argon2),
strategy: z.literal(ChallengeStrategy.TargetNumber),
target_number: z.object({
max_number: z.number().int().min(1).max(100_000),
}),
}),
]);
const KCTFSchema = z.object({
algorithm: z.literal(ChallengeAlgorithm.kCTF),
kctf: z.object({
difficulty: z.number().int().min(1),
}),
}); });
export const Config = z.union([SHA256Schema, Argon2Schema, KCTFSchema]);
export type Config = z.infer<typeof Config>; export type Config = z.infer<typeof Config>;
export const Config = z.discriminatedUnion('strategy', [
LeadingZeroesSchema,
TargetNumberSchema,
]);
export let config: Config; export let config: Config;
try { try {

View File

@@ -1,10 +1,14 @@
build: build-widget
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz" 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" npm-runner := "npm"
[working-directory: "example-app"]
playground: build
{{npm-runner}} run dev
build: build-widget
[working-directory: "solver"] [working-directory: "solver"]
build-wasm: build-wasm:
zig build {{zig-build-args}} zig build {{zig-build-args}}

View File

@@ -2058,7 +2058,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2332,7 +2331,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2383,7 +2381,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",

View File

@@ -1,16 +1,43 @@
import { UUID } from "uuidv7";
export enum ChallengeAlgorithm { export enum ChallengeAlgorithm {
Argon2id = "argon2id", SHA256 = "sha256",
Argon2 = "argon2",
kCTF = "kctf",
}
export function algorithmToInt(algorithm: ChallengeAlgorithm): number {
switch (algorithm) {
case ChallengeAlgorithm.SHA256:
return 0;
case ChallengeAlgorithm.Argon2:
return 1;
case ChallengeAlgorithm.kCTF:
return 2;
}
} }
export enum ChallengeStrategy { export enum ChallengeStrategy {
Null = "null",
LeadingZeroes = "leading_zeroes", LeadingZeroes = "leading_zeroes",
TargetNumber = "target_number", 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 // In this case, the client will repeatedly hash a number with has until it
// finds a hash thaat starts with *difficulty* leading zeroes // finds a hash thaat starts with *difficulty* leading zeroes
export interface ChallengeLeadingZeroes { export interface ChallengeLeadingZeroes {
algorithm: ChallengeAlgorithm; algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.LeadingZeroes; strategy: ChallengeStrategy.LeadingZeroes;
salt: string; // random string salt: string; // random string
difficulty: number; difficulty: number;
@@ -19,10 +46,24 @@ export interface ChallengeLeadingZeroes {
// In this case, the server generates a random number, and the client will hash // 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 // the salt (a random string) + a random number until it finds a hash that is equal to challenge
export interface ChallengeTargetNumber { export interface ChallengeTargetNumber {
algorithm: ChallengeAlgorithm; algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.TargetNumber; strategy: ChallengeStrategy.TargetNumber;
salt: string; // random string salt: string; // random string
target: string; // hash of salt + random number target: string; // hash of salt + random number
} }
export type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber; export interface InnerChallengekCTF {
algorithm: ChallengeAlgorithm.kCTF;
salt: UUID; // UUIDv7
difficulty: number;
}
export interface ChallengekCTF {
algorithm: ChallengeAlgorithm.kCTF;
salt: string;
difficulty: number;
}
export type InnerChallenge = InnerChallengekCTF | ChallengeLeadingZeroes | ChallengeTargetNumber;
export type Challenge = ChallengekCTF | ChallengeLeadingZeroes | ChallengeTargetNumber;

View File

@@ -1,10 +1,10 @@
import { ChallengeAlgorithm, ChallengeStrategy, algorithmToInt, strategyToInt } from "./index";
import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline'; import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
type WasmExports = Record<string, Function> & { type WasmExports = Record<string, Function> & {
"malloc": (byte_count: number) => number | null; "malloc": (byte_count: number) => number | null;
"free": (ptr: number | null, byte_count: number) => void; "free": (ptr: number | null, byte_count: number) => void;
"solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number; "solve": (algorithm: number, strategy: number, salt_ptr: number, salt_len: number, difficulty: number, target_ptr: number, target_len: number) => number,
"solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: number) => number;
"memory": WebAssembly.Memory; "memory": WebAssembly.Memory;
} }
@@ -17,6 +17,7 @@ export type SolverEnv = {
__set_solution: (value: number) => void; __set_solution: (value: number) => void;
__cmpxchg_solution: (expected: number, replacement: number) => number; __cmpxchg_solution: (expected: number, replacement: number) => number;
__fetch_add_nonce: (value: 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> { export async function get_wasm_module(): Promise<WebAssembly.Module> {
@@ -29,64 +30,100 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P
}) as unknown as SolverModule; }) as unknown as SolverModule;
} }
export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number { type Argon2LeadingZeroesParams = {
const { salt, difficulty } = challenge; name: ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.LeadingZeroes;
salt: string;
difficulty: number;
};
type Argon2TargetNumberParams = {
name: ChallengeAlgorithm.Argon2;
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 encoder = new TextEncoder();
let salt_buf = encoder.encode(algorithm.salt);
const salt_bytes = encoder.encode(salt); let salt_ptr = solver.exports.malloc(salt_buf.length);
const salt_ptr = solver.exports.malloc(salt_bytes.length);
if (salt_ptr === 0 || salt_ptr === null) { if (salt_ptr === 0 || salt_ptr === null) {
throw new Error("Failed to allocate memory for challenge string"); throw new Error("Failed to allocate memory for challenge string");
} }
const memory = new Uint8Array(solver.exports.memory.buffer); let memory = new Uint8Array(solver.exports.memory.buffer);
memory.set(salt_bytes, salt_ptr); memory.set(salt_buf, salt_ptr);
const ret = solver.exports.solve_leaading_zeroes_challenge( let ret: string | number;
salt_ptr, switch (algorithm.name) {
salt_bytes.length, case ChallengeAlgorithm.SHA256:
difficulty, case ChallengeAlgorithm.Argon2:
); switch (algorithm.strategy) {
case ChallengeStrategy.LeadingZeroes: {
if (ret < 0) { ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.LeadingZeroes), salt_ptr, salt_buf.length, algorithm.difficulty, 0, 0);
throw new Error("Failed to solve challenge"); break;
} }
case ChallengeStrategy.TargetNumber: {
return ret; const target_buf = encoder.encode(algorithm.target);
} const target_ptr = solver.exports.malloc(target_buf.length);
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) { if (target_ptr === 0 || target_ptr === null) {
throw new Error("Failed to allocate memory for target string"); throw new Error("Failed to allocate memory for target string");
} }
const memory = new Uint8Array(solver.exports.memory.buffer); memory = new Uint8Array(solver.exports.memory.buffer);
memory.set(salt_bytes, salt_ptr); memory.set(target_buf, target_ptr);
memory.set(target_bytes, target_ptr);
const ret = solver.exports.solve_target_number_challenge( ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.TargetNumber), salt_ptr, salt_buf.length, 0, target_ptr, target_buf.length);
target_ptr, break;
target_bytes.length, }
salt_ptr, }
salt_bytes.length, 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 (ret < 0) { if (solution_ptr <= 0) {
throw new Error("Failed to solve challenge"); 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;
}
return ret; return ret;
} }

View File

@@ -1,12 +1,12 @@
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.'; import { ChallengeStrategy, type Challenge, type InnerChallenge, ChallengeAlgorithm, algorithmToInt, strategyToInt } from '.';
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline'; import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
import { uuidv7obj } from 'uuidv7';
type WasmExports = Record<string, Function> & { type WasmExports = Record<string, Function> & {
"malloc": (byte_count: number) => number | null; "malloc": (byte_count: number) => number | null;
"free": (ptr: number | null, byte_count: number) => void; "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": (algorithm: number, strategy: number, challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number, nonce: number, difficulty: number) => boolean;
"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, challebge_len: number, nonce_ptr: number, nonce_len: number, algorithm: number) => bigint;
"hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint;
"memory": WebAssembly.Memory; "memory": WebAssembly.Memory;
} }
@@ -14,159 +14,281 @@ export interface ValidatorModule extends WebAssembly.Instance {
exports: WasmExports; exports: WasmExports;
} }
function array_to_base64(buffer: ArrayBuffer): string { function array_to_base64(buffer: ArrayBufferLike): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer))); let binary = '';
const bytes = new Uint8Array(buffer);
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
} }
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<Challenge> { export interface SHA256ChallengeConfig {
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&"); algorithm: ChallengeAlgorithm.SHA256;
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`; strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber;
difficulty: number;
parameters: Object;
}
let challenge: Challenge = { export interface Argon2ChallengeConfig {
algorithm: ChallengeAlgorithm.Argon2id, algorithm: ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.LeadingZeroes, strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber;
salt, difficulty: number;
difficulty, parameters: Object;
}; }
export interface kCTFChallengeConfig {
algorithm: ChallengeAlgorithm.kCTF;
difficulty: number;
parameters: Object;
}
export type ChallengeConfig = kCTFChallengeConfig | SHA256ChallengeConfig | Argon2ChallengeConfig;
async function encode_challenge(inner_challenge: InnerChallenge, parameters: Object = {}): Promise<Challenge> {
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.Argon2: {
challenge.algorithm = ChallengeAlgorithm.Argon2;
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;
}
}
// 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;
}
challenge.salt = challenge.salt + parameters_str;
return challenge; return challenge;
} }
async function generate_target_number_challenge(parameters: Object, max_number: number): Promise<Challenge | null> { export async function generate_challenge(config: ChallengeConfig): Promise<Challenge | null> {
// in target number config, since we need to generate a target hash, we if (config.difficulty < 1) {
// need to hash the salt + nonce, so the client knows what the target is throw new Error("Difficulty must be at least 1");
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule; }
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&"); const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl), {
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`; env: {
let random_number = new DataView(crypto.getRandomValues(new Uint8Array(4)).buffer).getUint32(0, true) % max_number; __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 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); var inner_challenge: InnerChallenge = {
const random_number_ptr = validator.exports.malloc(random_number_bytes.length); 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.Argon2:
switch (config.strategy) {
case ChallengeStrategy.LeadingZeroes:
if (config.difficulty < 1 || config.difficulty > 64) {
throw new Error("Invalid difficulty for leading zeroes strategy");
}
if (salt_ptr === 0 || salt_ptr === null || random_number_ptr === 0 || random_number_ptr === null) { // @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"); console.error("Failed to allocate memory for challenge string");
return null; return null;
} }
const memory = new Uint8Array(validator.exports.memory.buffer); const memory = new Uint8Array(validator.exports.memory.buffer);
memory.set(salt_bytes, salt_ptr); memory.set(challenge_buf, challenge_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); const challenge_len = inner_challenge.salt.length;
let target_ptr = Number(target_blob & BigInt(0xFFFFFFFF)); const nonce_ptr = challenge_ptr + challenge_len;
let target_len = Number(target_blob >> BigInt(32)); const nonce_len = challenge_buf.length - challenge_len;
validator.exports.free(salt_ptr, salt_bytes.length); const target = validator.exports.hash(challenge_ptr, challenge_len, nonce_ptr, nonce_len, algorithmToInt(inner_challenge.algorithm));
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 const target_len = Number((target >> 32n) & 0xFFFFFFFFn);
let target_slice = new Uint8Array(validator.exports.memory.buffer.slice(target_ptr, target_ptr + target_len)); const target_ptr = Number(target & 0xFFFFFFFFn);
const target = new TextDecoder().decode(target_slice);
let challenge: Challenge = { const target_buf = new Uint8Array(validator.exports.memory.buffer, target_ptr, target_len);
algorithm: ChallengeAlgorithm.Argon2id, // @ts-ignore
strategy: ChallengeStrategy.TargetNumber, inner_challenge.target = new TextDecoder().decode(target_buf);
salt, // @ts-ignore
target console.log("TARGET", inner_challenge.target);
};
return challenge; validator.exports.free(challenge_ptr, challenge_len + random_number.length);
} validator.exports.free(target_ptr, target_len);
export interface LeadingZeroesChallengeConfig {
parameters: Object;
strategy: ChallengeStrategy.LeadingZeroes;
difficulty: number;
}
export interface TargetNumberChallengeConfig {
parameters: Object;
strategy: ChallengeStrategy.TargetNumber;
max_number: number;
}
export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig;
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; break;
case ChallengeStrategy.TargetNumber: }
challenge = await generate_target_number_challenge(config.parameters, config.max_number); 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; break;
} }
if (challenge === null) { return await encode_challenge(inner_challenge, config.parameters);
return null;
}
return challenge;
} }
export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise<boolean> { export async function validate_challenge(challenge: Challenge, challenge_solution: string | number): Promise<boolean> {
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule 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 encoder = new TextEncoder();
let err; if (challenge.algorithm === ChallengeAlgorithm.kCTF) {
let memory; challenge.salt = challenge.salt.split("?")[0];
let nonce_bytes, nonce_ptr; }
let target_bytes, target_ptr;
const challenge_buf = encoder.encode(challenge.salt);
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 false;
}
const memory = new Uint8Array(validator.exports.memory.buffer);
memory.set(challenge_buf, challenge_ptr);
switch (challenge.algorithm) {
case ChallengeAlgorithm.SHA256:
if (typeof challenge_solution === "string") {
throw new Error("Argon2 challenges do not support a solution as a number");
}
switch (challenge.strategy) { switch (challenge.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
target_bytes = encoder.encode(challenge_solution.challenge); return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, 0, 0, challenge_solution, challenge.difficulty);
nonce_bytes = encoder.encode(challenge_solution.nonce);
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: case ChallengeStrategy.TargetNumber:
target_bytes = encoder.encode(challenge.target); const solution_buf = encoder.encode(challenge.target);
const salt_bytes = encoder.encode(challenge.salt); const solution_ptr = validator.exports.malloc(solution_buf.length);
nonce_bytes = encoder.encode(challenge_solution.nonce); if (solution_ptr === 0 || solution_ptr === null) {
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"); console.error("Failed to allocate memory for challenge string");
return false; return false;
} }
memory = new Uint8Array(validator.exports.memory.buffer); const memory = new Uint8Array(validator.exports.memory.buffer);
memory.set(salt_bytes, salt_ptr); memory.set(solution_buf, solution_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); return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, solution_ptr, solution_buf.length, challenge_solution, 0);
}
validator.exports.free(salt_ptr, salt_bytes.length); case ChallengeAlgorithm.Argon2:
validator.exports.free(target_ptr, target_bytes.length); if (typeof challenge_solution === "string") {
validator.exports.free(nonce_ptr, nonce_bytes.length); throw new Error("Argon2 challenges do not support a solution as a number");
break;
} }
return err === 0; 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);
}
} }

View File

@@ -1433,7 +1433,6 @@
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -2148,7 +2147,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2422,7 +2420,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2461,7 +2458,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",

View File

@@ -1,7 +1,7 @@
import { LitElement, html, css, isServer, type PropertyValues } from 'lit'; import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker'; 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 { get_wasm_module } from '@impost/lib/solver';
import ChallengeWorker from './solver-worker?worker&inline'; import ChallengeWorker from './solver-worker?worker&inline';
@@ -21,6 +21,9 @@ export class PowCaptcha extends LitElement {
background-color: var(--impost-widget-background-color, #070408); background-color: var(--impost-widget-background-color, #070408);
border-radius: var(--impost-widget-border-radius, 8px); border-radius: var(--impost-widget-border-radius, 8px);
} }
.impost-error-icon {
color: var(--impost-widget-error-icon-color, #FF8117);
}
.impost-main { .impost-main {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -86,38 +89,29 @@ export class PowCaptcha extends LitElement {
@state() @state()
private solution: string = ''; private solution: string = '';
@state()
private errorMessage: string = '';
@state() @state()
private challengeData: Challenge | null = null; private challengeData: Challenge | null = null;
@state() @state()
private solved: boolean = false; private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
@state()
private isSolving: boolean = false;
@state() @state()
private disabled: boolean = true; private disabled: boolean = true;
@state()
private hashRate: number = 0;
// stores the nonce and solution atomics // stores the nonce and solution atomics
private sab: SharedArrayBuffer = new SharedArrayBuffer(8); private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
private solverWorkers: Worker[] | null = null; private solverWorkers: Worker[] | null = null;
private solveStartTime: number | null = null;
private hashRateInterval: number | null = null;
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.fetchChallenge(); this.fetchChallenge();
this.initWorkers(); this.initWorkers();
this.addEventListener('reset', (ev) => this.reset(ev as CustomEvent));
this.addEventListener('solve', () => this.solveChallenge());
switch (this.auto) { switch (this.auto) {
case 'onload': case 'onload':
this.solveChallenge(); this.solveChallenge();
@@ -141,10 +135,6 @@ export class PowCaptcha extends LitElement {
override disconnectedCallback() { override disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.hashRateInterval !== null) {
clearInterval(this.hashRateInterval);
this.hashRateInterval = null;
}
for (const worker of this.solverWorkers || []) { for (const worker of this.solverWorkers || []) {
worker.terminate(); worker.terminate();
@@ -152,12 +142,24 @@ export class PowCaptcha extends LitElement {
} }
} }
reset(ev: CustomEvent) {
this.challengejson = JSON.stringify(ev.detail.challenge);
this.challengeData = null;
this.status = 'unsolved';
this.solution = '';
console.log("received reset event");
this.fetchChallenge();
console.log(this.challengeData);
}
getCurrentWorkingNonce() { getCurrentWorkingNonce() {
return Atomics.load(new Uint32Array(this.sab), 0); return Atomics.load(new Uint32Array(this.sab), 0);
} }
async fetchChallenge() { async fetchChallenge() {
this.errorMessage = '';
if (this.challengeData !== null) { if (this.challengeData !== null) {
return; return;
} }
@@ -179,22 +181,18 @@ export class PowCaptcha extends LitElement {
}) })
.catch(error => { .catch(error => {
console.error('Error fetching challenge:', error); console.error('Error fetching challenge:', error);
this.errorMessage = 'Failed to fetch challenge. Please try again.'; console.error('Failed to fetch challenge');
}); });
} }
async initWorkers() { async initWorkers() {
this.solverWorkers = []; this.solverWorkers = [];
const num_workers = navigator.hardwareConcurrency; const num_workers = navigator.hardwareConcurrency || 4;
for (let i = 0; i < num_workers; i++) { for (let i = 0; i < num_workers; i++) {
this.solverWorkers.push(new ChallengeWorker()); this.solverWorkers.push(new ChallengeWorker());
} }
const atomics_view = new Int32Array(this.sab);
Atomics.store(atomics_view, 0, 0);
Atomics.store(atomics_view, 1, 0);
let wasm_module = await get_wasm_module(); let wasm_module = await get_wasm_module();
let worker_promises: Promise<void>[] = []; let worker_promises: Promise<void>[] = [];
for (let i = 0; i < this.solverWorkers.length; i++) { for (let i = 0; i < this.solverWorkers.length; i++) {
@@ -237,7 +235,8 @@ export class PowCaptcha extends LitElement {
let timeout: number; let timeout: number;
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => { 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`)); reject(new Error(`Function timed out after ${timeoutMs}ms`));
}, timeoutMs); }, timeoutMs);
}); });
@@ -272,9 +271,28 @@ export class PowCaptcha extends LitElement {
worker.addEventListener('message', message_handler); worker.addEventListener('message', message_handler);
worker.addEventListener('error', error_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.algorithm) {
case ChallengeAlgorithm.SHA256:
switch (request.strategy) { switch (request.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
worker.postMessage({ worker.postMessage({
algorithm: ChallengeAlgorithm.SHA256,
strategy: ChallengeStrategy.LeadingZeroes, strategy: ChallengeStrategy.LeadingZeroes,
salt: request.salt, salt: request.salt,
difficulty: request.difficulty, difficulty: request.difficulty,
@@ -282,18 +300,49 @@ export class PowCaptcha extends LitElement {
break; break;
case ChallengeStrategy.TargetNumber: case ChallengeStrategy.TargetNumber:
worker.postMessage({ worker.postMessage({
algorithm: ChallengeAlgorithm.SHA256,
strategy: ChallengeStrategy.TargetNumber, strategy: ChallengeStrategy.TargetNumber,
target: request.target, target: request.target,
salt: request.salt, salt: request.salt,
} as WorkerRequest); } as WorkerRequest);
break; break;
} }
break;
case ChallengeAlgorithm.Argon2:
switch (request.strategy) {
case ChallengeStrategy.LeadingZeroes:
worker.postMessage({
algorithm: ChallengeAlgorithm.Argon2,
strategy: ChallengeStrategy.LeadingZeroes,
salt: request.salt,
difficulty: request.difficulty,
} as WorkerRequest);
break;
case ChallengeStrategy.TargetNumber:
worker.postMessage({
algorithm: ChallengeAlgorithm.Argon2,
strategy: ChallengeStrategy.TargetNumber,
target: request.target,
salt: request.salt,
} as WorkerRequest);
break;
}
break;
case ChallengeAlgorithm.kCTF:
worker.postMessage({
algorithm: ChallengeAlgorithm.kCTF,
salt: request.salt,
difficulty: request.difficulty,
} as WorkerRequest);
break;
}
}); });
} }
async solveChallenge() { async solveChallenge() {
if (!this.challengeData || this.solverWorkers === null) { 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; return;
} }
@@ -301,25 +350,14 @@ export class PowCaptcha extends LitElement {
return; 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', { this.dispatchEvent(new CustomEvent('impost:solve', {
detail: {
solution: this.solution,
},
bubbles: true, bubbles: true,
composed: true, composed: true,
})) }))
console.log(this.challengeData); console.log(this.challengeData);
this.isSolving = true; this.status = 'solving';
this.errorMessage = '';
this.solution = ''; this.solution = '';
const atomics_view = new Int32Array(this.sab); const atomics_view = new Int32Array(this.sab);
@@ -328,9 +366,12 @@ export class PowCaptcha extends LitElement {
let request: ChallengeSolveRequest; let request: ChallengeSolveRequest;
switch (this.challengeData.algorithm) {
case ChallengeAlgorithm.SHA256:
switch (this.challengeData.strategy) { switch (this.challengeData.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
request = { request = {
algorithm: ChallengeAlgorithm.SHA256,
strategy: ChallengeStrategy.LeadingZeroes, strategy: ChallengeStrategy.LeadingZeroes,
salt: this.challengeData.salt, salt: this.challengeData.salt,
difficulty: this.challengeData.difficulty, difficulty: this.challengeData.difficulty,
@@ -338,54 +379,101 @@ export class PowCaptcha extends LitElement {
break; break;
case ChallengeStrategy.TargetNumber: case ChallengeStrategy.TargetNumber:
request = { request = {
algorithm: ChallengeAlgorithm.SHA256,
strategy: ChallengeStrategy.TargetNumber, strategy: ChallengeStrategy.TargetNumber,
target: this.challengeData.target, target: this.challengeData.target,
salt: this.challengeData.salt, salt: this.challengeData.salt,
}; };
break; break;
} }
break;
case ChallengeAlgorithm.Argon2:
switch (this.challengeData.strategy) {
case ChallengeStrategy.LeadingZeroes:
request = {
algorithm: ChallengeAlgorithm.Argon2,
strategy: ChallengeStrategy.LeadingZeroes,
salt: this.challengeData.salt,
difficulty: this.challengeData.difficulty,
};
break;
case ChallengeStrategy.TargetNumber:
request = {
algorithm: ChallengeAlgorithm.Argon2,
strategy: ChallengeStrategy.TargetNumber,
target: this.challengeData.target,
salt: this.challengeData.salt,
};
break;
}
break;
case ChallengeAlgorithm.kCTF:
request = {
algorithm: ChallengeAlgorithm.kCTF,
salt: this.challengeData.salt,
difficulty: this.challengeData.difficulty,
};
break;
}
console.log('Sending challenge to workers...'); console.log('Sending challenge to workers...');
// TODO: the first response is not always the solution, due to cmpxchg // TODO: the first response is not always the solution, due to cmpxchg
// blocking, some workers may block on the read, and as soon as they // blocking, some workers may block on the read, and as soon as they
// unblock, they return 0 since the challenge is already solved. // 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. // errors out, we only error out if all workers have errored out.
let worker_promises: Promise<SolutionMessage>[] = []; let worker_promises: Promise<SolutionMessage>[] = [];
if (request.algorithm === ChallengeAlgorithm.kCTF) {
worker_promises.push(this.issueChallengeToWorker(this.solverWorkers[0], request));
} else {
for (let worker of this.solverWorkers) { for (let worker of this.solverWorkers) {
// dispatch to all workers, func is async so it will not block // dispatch to all workers, func is async so it will not block
worker_promises.push(this.issueChallengeToWorker(worker, request)); worker_promises.push(this.issueChallengeToWorker(worker, request));
} }
}
let solution = await Promise.race(worker_promises); let solution = await Promise.race(worker_promises);
if (solution.type === WorkerResponseType.Error) { if (solution.type === WorkerResponseType.Error) {
this.errorMessage = solution.error; console.error("Worker error:", solution.error);
this.status = 'error';
return; return;
} }
if (solution.type !== WorkerResponseType.Solution) { if (solution.type !== WorkerResponseType.Solution) {
this.errorMessage = "Something went wrong, please try again later."; console.error("Worker sent spurious message");
this.status = 'error';
return; return;
} }
this.solution = Atomics.load(atomics_view, 1).toString(); // TODO: configure if we should fetch or not
this.isSolving = false; try {
this.solved = true; await fetch(`${this.challengeUrl}/challenge`, {
method: 'POST',
if (this.hashRateInterval !== null) { body: JSON.stringify({
clearInterval(this.hashRateInterval); salt: this.challengeData.salt,
this.hashRateInterval = null; solution: solution.solution,
}),
headers: {
'Content-Type': 'application/json'
} }
})
this.status = 'solved';
this.dispatchEvent(new CustomEvent('impost:solved', { this.dispatchEvent(new CustomEvent('impost:solved', {
detail: { detail: {
solution: this.solution, salt: this.challengeData.salt,
solution: solution.solution,
}, },
bubbles: true, bubbles: true,
composed: true, composed: true,
})) }))
} catch {
console.error('Failed to submit solution');
this.status = 'error';
}
} }
solvePreventDefault(event: Event) { solvePreventDefault(event: Event) {
@@ -400,12 +488,6 @@ export class PowCaptcha extends LitElement {
this.challengejson = ''; this.challengejson = '';
} }
if (this.errorMessage) {
return html`
<div class="error-message">${this.errorMessage}</div>
`;
}
if (this.challengeData === null) { if (this.challengeData === null) {
return html` return html`
<div class="loading-message">Loading captcha challenge...</div> <div class="loading-message">Loading captcha challenge...</div>
@@ -416,9 +498,9 @@ export class PowCaptcha extends LitElement {
<div class="impost-widget"> <div class="impost-widget">
<div class="impost-main"> <div class="impost-main">
<div class="impost-checkbox"> <div class="impost-checkbox">
${!this.isSolving ? 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.solved}> <input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
` : html` `}` : html`
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"> <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 --> <!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
<g stroke="currentColor"> <g stroke="currentColor">
@@ -431,16 +513,10 @@ export class PowCaptcha extends LitElement {
</svg> </svg>
`} `}
</div> </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>
<div class="impost-footer"> <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"> <div id="provider-link">
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a> Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
</div> </div>

View File

@@ -8,9 +8,8 @@ import {
WorkerResponseType, WorkerResponseType,
} from "./types/worker"; } from "./types/worker";
import { ChallengeStrategy } from "@impost/lib"; import { type SolverModule, init_solver, solve, type SolveParams } from '@impost/lib/solver';
import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
let solver: SolverModule | null = null; let solver: SolverModule | null = null;
@@ -29,6 +28,7 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value), __set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement), __cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value), __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); }, module);
} catch (error: any) { } catch (error: any) {
postMessage({ postMessage({
@@ -60,32 +60,56 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
return; return;
} }
if (atomic_nonce === null || atomic_solution === null) { let solution: string | number;
throw new Error("Atomics not initialized"); try {
} let params = {
name: event.data.algorithm,
salt: event.data.salt,
};
const { strategy } = event.data; switch (event.data.algorithm) {
case ChallengeAlgorithm.Argon2:
let solution: number; case ChallengeAlgorithm.SHA256:
switch (strategy) { switch (event.data.strategy) {
case ChallengeStrategy.LeadingZeroes: case ChallengeStrategy.LeadingZeroes:
solution = solve_leaading_zeroes_challenge(solver, event.data) // @ts-ignore
params.strategy = ChallengeStrategy.LeadingZeroes;
// @ts-ignore
params.difficulty = event.data.difficulty;
break; break;
case ChallengeStrategy.TargetNumber: case ChallengeStrategy.TargetNumber:
solution = solve_target_number_challenge(solver, event.data) // @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; break;
} }
// we are just assuming that if its less than -1, its the min i32 solution = solve(solver, params as SolveParams);
if (solution < 0) {
return postMessage({ if (event.data.algorithm !== ChallengeAlgorithm.kCTF) {
type: WorkerResponseType.Error, console.log(Atomics.load(atomic_nonce!, 0));
error: "failed to solve challenge", solution = Atomics.load(atomic_solution!, 0);
} as SolutionMessage);
} }
} catch (error: any) {
postMessage({
type: WorkerResponseType.Error,
error: `Failed to solve challenge: ${error.message}`,
} as SolutionMessage);
return;
}
postMessage({ postMessage({
type: WorkerResponseType.Solution, type: WorkerResponseType.Solution,
nonce: solution === -1 ? null : solution.toString() solution,
} as SolutionMessage); } as SolutionMessage);
}; };

View File

@@ -1,4 +1,4 @@
import { ChallengeStrategy } from "@impost/lib"; import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib";
export enum WorkerMessageType { export enum WorkerMessageType {
Init = "init", Init = "init",
@@ -13,6 +13,7 @@ interface WorkerInitRequest {
} }
interface ChallengeLeadingZeroesSolveRequest { interface ChallengeLeadingZeroesSolveRequest {
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.LeadingZeroes; strategy: ChallengeStrategy.LeadingZeroes;
salt: string; salt: string;
difficulty: number; difficulty: number;
@@ -23,6 +24,7 @@ interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroe
} }
interface ChallengeTargetNumberSolveRequest { interface ChallengeTargetNumberSolveRequest {
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
strategy: ChallengeStrategy.TargetNumber; strategy: ChallengeStrategy.TargetNumber;
target: string; target: string;
salt: string; salt: string;
@@ -32,8 +34,18 @@ interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberS
type: WorkerMessageType.Challenge; type: WorkerMessageType.Challenge;
} }
export type ChallengeSolveRequest = ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest; interface ChallengekCTFSolveRequest {
type WorkerChallengeSolveRequest = WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest; algorithm: ChallengeAlgorithm.kCTF;
salt: string;
difficulty: number;
}
interface WorkerChallengekCTFSolveRequest extends ChallengekCTFSolveRequest {
type: WorkerMessageType.Challenge;
}
export type ChallengeSolveRequest = ChallengekCTFSolveRequest | ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest;
type WorkerChallengeSolveRequest = WorkerChallengekCTFSolveRequest | WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest;
export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest; export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
@@ -50,7 +62,7 @@ interface ErrorMessageResponse {
interface SolutionMessageResponse { interface SolutionMessageResponse {
type: WorkerResponseType.Solution; type: WorkerResponseType.Solution;
nonce: string; solution: string | number;
} }
interface InitOkMessageResponse { interface InitOkMessageResponse {

View File

@@ -2,17 +2,13 @@ const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const wasm_target = b.resolveTargetQuery(.{ const target = b.standardTargetOptions(.{});
.cpu_arch = .wasm32,
.os_tag = .freestanding,
.cpu_features_add = std.Target.wasm.featureSet(&.{ .bulk_memory, .bulk_memory_opt, .simd128, .tail_call }),
});
// solver // solver
const solver_mod = b.addModule("solver", .{ const solver_mod = b.addModule("solver", .{
.root_source_file = b.path("src/solver.zig"), .root_source_file = b.path("src/solver.zig"),
.optimize = optimize, .optimize = optimize,
.target = wasm_target, .target = target,
}); });
const solver_exe = b.addExecutable(.{ const solver_exe = b.addExecutable(.{
@@ -20,10 +16,12 @@ pub fn build(b: *std.Build) void {
.root_module = solver_mod, .root_module = solver_mod,
}); });
if (target.result.cpu.arch == .wasm32) {
solver_exe.entry = .disabled; solver_exe.entry = .disabled;
solver_exe.rdynamic = true; solver_exe.rdynamic = true;
solver_exe.lto = .full;
solver_exe.link_gc_sections = true; solver_exe.link_gc_sections = true;
solver_exe.lto = .full;
}
b.installArtifact(solver_exe); b.installArtifact(solver_exe);
@@ -31,16 +29,18 @@ pub fn build(b: *std.Build) void {
const validator_mod = b.addModule("validator", .{ const validator_mod = b.addModule("validator", .{
.root_source_file = b.path("src/validator.zig"), .root_source_file = b.path("src/validator.zig"),
.optimize = optimize, .optimize = optimize,
.target = wasm_target, .target = target,
}); });
const validator_exe = b.addLibrary(.{ const validator_exe = b.addExecutable(.{
.name = "validator", .name = "validator",
.root_module = validator_mod, .root_module = validator_mod,
}); });
if (target.result.cpu.arch == .wasm32) {
validator_exe.entry = .disabled; validator_exe.entry = .disabled;
validator_exe.rdynamic = true; validator_exe.rdynamic = true;
}
b.installArtifact(validator_exe); b.installArtifact(validator_exe);
} }

View File

@@ -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");

View File

@@ -0,0 +1,17 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
var argon2_params = std.crypto.pwhash.argon2.Params{
.t = 3, // time cost
.m = 8192, // memory cost (in KiB)
.p = 1, // parallelism
};
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, .argon2d);
return derived;
}

View File

@@ -0,0 +1,155 @@
// 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);
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);
const usize_salt_bytes: []align(1) usize = std.mem.bytesAsSlice(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) |_| {
for (0..1277) |_| {
try square_mod(&self.salt);
}
try self.salt.bitXor(&self.salt, &managed_one.?);
}
return try self.encode(allocator);
}
pub fn verify(self: *Self, allocator: Allocator, solution: *Challenge) !bool {
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);
}
// 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;
}
};

View File

@@ -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;
}

View File

@@ -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..];
}

View File

@@ -1,7 +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;

View File

@@ -1,23 +1,40 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const hasher = @import("hasher.zig"); const algorithms = @import("algorithms/algorithms.zig");
const utils = @import("utils.zig");
// var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
// var allocator = gpa.allocator(); var allocator = gpa.allocator();
var allocator = std.heap.wasm_allocator;
extern fn __get_solution() i32; extern fn __get_solution() i32;
extern fn __set_solution(value: i32) void; extern fn __set_solution(value: i32) void;
extern fn __cmpxchg_solution(old: i32, new: i32) i32; extern fn __cmpxchg_solution(old: i32, new: i32) i32;
extern fn __fetch_add_nonce(value: i32) i32; extern fn __fetch_add_nonce(value: i32) i32;
extern fn __log(str_ptr: usize, str_len: usize) void;
// fn log(comptime fmt: []const u8, args: anytype) void { fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
// const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return; if (comptime builtin.target.cpu.arch != .wasm32) {
// __log(@intFromPtr(formatted.ptr), formatted.len); std.log.defaultLog(level, scope, fmt, args);
// allocator.free(formatted); 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;
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 };
export fn malloc(byte_count: usize) ?*u8 { export fn malloc(byte_count: usize) ?*u8 {
const ptr = allocator.alloc(u8, byte_count) catch return null; const ptr = allocator.alloc(u8, byte_count) catch return null;
@@ -31,50 +48,66 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
} }
} }
const SolveError = enum(u32) { /// Both SHA256 and Argon2 are thread safe and are explicitly designed to be used in a multithreaded environment.
InvalidDifficulty = 1, /// kCTF is designed only to be used in a single threaded environment. It does not use the same nonce atomics,
InvalidNonce = 2, /// and duplicates work if solved across multiple threads.
NoSolution = 3, ///
OutOfMemory = 4, /// 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 {
switch (algorithm) {
var solve_error: ?SolveError = null; algorithms.Algorithm.sha256 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len),
export fn get_solve_error() u32 { algorithms.Algorithm.argon2 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len),
if (solve_error) |err| { algorithms.Algorithm.kctf => {
return @intFromEnum(err); if (strategy != algorithms.Strategy.null) {
} std.log.err("kCTF does not support a strategy", .{});
return 0;
}
// returns nonce on success, -1 on failure
// to get the error, call get_solve_error
export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, difficulty: u32) i32 {
solve_error = null;
const challenge_slice = challenge_ptr[0..challenge_len];
if (difficulty < 1 or difficulty > 64) {
solve_error = SolveError.InvalidDifficulty;
return -1; return -1;
} }
var target_prefix_buffer: [64]u8 = @splat('0'); return solve_kctf(salt_ptr, salt_len, difficulty);
const target_prefix = target_prefix_buffer[0..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 = 1_000_000_000;
// const max_nonce_iterations: u64 = 100_000;
// 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations) // 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, challenge_len + 9) catch { var input_buffer: []u8 = allocator.alloc(u8, salt_len + 9) catch {
// log("Failed to allocate memory for challenge\n", .{}); std.log.err("Out of memory", .{});
solve_error = SolveError.OutOfMemory;
return -1; return -1;
}; };
// dont leak memory :pepega: // dont leak memory :pepega:
defer allocator.free(input_buffer); defer allocator.free(input_buffer);
@memcpy(input_buffer[0..challenge_len], challenge_slice); @memcpy(input_buffer[0..salt_len], salt_slice);
var nonce = __fetch_add_nonce(1); 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)) { while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
if (__get_solution() != -1) { if (__get_solution() != -1) {
@@ -82,67 +115,28 @@ export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: u
return 0; return 0;
} }
const nonce_str = std.fmt.bufPrint(input_buffer[challenge_len..], "{d}", .{nonce}) catch {
solve_error = SolveError.InvalidNonce;
return -1;
};
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
solve_error = SolveError.OutOfMemory;
return -1;
};
if (std.mem.startsWith(u8, hash_hex_slice, target_prefix)) {
// 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;
}
}
}
solve_error = SolveError.NoSolution;
return -1;
}
export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, salt_ptr: [*]u8, salt_len: usize) i32 {
solve_error = null;
const target_slice = target_ptr[0..target_len];
const salt_slice = salt_ptr[0..salt_len];
// TODO: take in max number
const max_nonce_iterations: usize = 1_000_000_000;
const max_digits = std.math.log10(max_nonce_iterations);
var input_buffer: []u8 = allocator.alloc(u8, salt_len + max_digits) catch {
solve_error = SolveError.OutOfMemory;
return -1;
};
defer allocator.free(input_buffer);
@memcpy(input_buffer[0..salt_len], salt_slice);
var nonce = __fetch_add_nonce(1);
while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
if (__get_solution() != -1) {
return 0;
}
const nonce_str = std.fmt.bufPrint(input_buffer[salt_len..], "{d}", .{nonce}) catch { const nonce_str = std.fmt.bufPrint(input_buffer[salt_len..], "{d}", .{nonce}) catch {
solve_error = SolveError.InvalidNonce; std.log.err("Error formatting nonce", .{});
return -1; return -1;
}; };
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch { if (algorithm == .argon2) {
solve_error = SolveError.OutOfMemory; 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; return -1;
}; };
} else {
input = algorithms.SHA256.hash(allocator, input_buffer[0 .. salt_len + nonce_str.len]) catch {
std.log.err("Error hashing salt", .{});
return -1;
};
}
if (std.mem.eql(u8, target_slice, hash_hex_slice)) { switch (strategy) {
.leading_zeros => {
_ = hex_encoder.encode(input);
allocator.free(input);
if (hex_encoder.countZeroes(difficulty)) {
// Found a solution! // Found a solution!
if (__cmpxchg_solution(-1, nonce) == -1) { if (__cmpxchg_solution(-1, nonce) == -1) {
// we found a solution, and we are the first to do so // we found a solution, and we are the first to do so
@@ -152,8 +146,146 @@ export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, sa
return 0; return 0;
} }
} }
},
.target_number => {
const hex = hex_encoder.encode(input);
allocator.free(input);
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;
},
}
} }
solve_error = SolveError.NoSolution; return 0;
return -1; }
// 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];
const challenge = algorithms.kCTF.Challenge.from_string(allocator, challenge_slice, difficulty) catch |err| {
std.log.info("Error decoding challenge: {s}\n", .{@errorName(err)});
return -1;
};
defer challenge.destroy(allocator);
const solution = challenge.solve(allocator) catch |err| {
std.log.info("Error solving challenge: {s}\n", .{@errorName(err)});
return -1;
};
const output_ptr = allocator.alloc(u8, solution.len + 4) catch return 0;
var output_slice = output_ptr[0 .. solution.len + 2];
if (output_slice.len - 2 > std.math.maxInt(u16)) {
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 .. 2 + solution.len], solution);
allocator.free(solution);
return @intCast(@intFromPtr(output_ptr.ptr));
}
pub fn main() anyerror!void {
if (comptime builtin.cpu.arch == .wasm32) return;
var args = try std.process.argsAlloc(allocator);
if (args.len < 2) {
std.log.err("Usage: {s} <algorithm> [options] <challenge>", .{args[0]});
return;
}
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 <strategy>: Specify the strategy to use. This only applies to some algorithms.\n", .{});
std.log.info(" --target <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} <strategy> [options] <challenge>", .{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});
} }

47
solver/src/utils.zig Normal file
View File

@@ -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];
}
}

View File

@@ -1,9 +1,27 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const hasher = @import("hasher.zig"); const algorithms = @import("algorithms/algorithms.zig");
const utils = @import("utils.zig");
var allocator = std.heap.wasm_allocator; var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var allocator = gpa.allocator();
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 {
if (comptime builtin.target.cpu.arch != .wasm32) {
std.log.defaultLog(level, scope, fmt, args);
return;
}
const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
__log(@intFromPtr(formatted.ptr), formatted.len);
allocator.free(formatted);
}
pub const std_options: std.Options = .{ .logFn = log };
export fn malloc(byte_count: usize) ?*u8 { export fn malloc(byte_count: usize) ?*u8 {
const ptr = allocator.alloc(u8, byte_count) catch return null; const ptr = allocator.alloc(u8, byte_count) catch return null;
@@ -17,65 +35,143 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
} }
} }
fn bytesToHex(bytes: []const u8, buf: []u8) void { 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 {
const hex_chars = "0123456789abcdef"; switch (algorithm) {
var i: usize = 0; algorithms.Algorithm.sha256 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.sha256, strategy),
while (i < bytes.len) : (i += 1) { algorithms.Algorithm.argon2 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.argon2, strategy),
buf[i * 2] = hex_chars[(bytes[i] >> 4)]; algorithms.Algorithm.kctf => return validate_kctf(challenge_ptr, challenge_len, solution_ptr, solution_len, difficulty),
buf[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
} }
} }
export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, difficulty: u32) i32 { 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 {
const challenge_slice = challenge_ptr[0..challenge_len]; if (strategy == algorithms.Strategy.null) {
const nonce_slice = nonce_ptr[0..nonce_len]; return false;
}
if (strategy == .leading_zeros) {
if (difficulty < 1 or difficulty > 64) { if (difficulty < 1 or difficulty > 64) {
return -1; return false;
}
} }
var target_prefix_buffer: [64]u8 = @splat('0'); const challenge_slice = challenge_ptr[0..challenge_len];
const target_prefix = target_prefix_buffer[0..difficulty]; const nonce_slice = std.fmt.allocPrint(allocator, "{d}", .{nonce}) catch return false;
const hash_hex_slice = hasher.hash(allocator, challenge_slice, nonce_slice) catch return -2; var target_slice: ?[]u8 = null;
if (@intFromPtr(target_ptr) != 0) {
if (!std.mem.startsWith(u8, hash_hex_slice, target_prefix)) { target_slice = target_ptr[0..target_len];
return -3;
} }
return 0; 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;
} else {
input = algorithms.SHA256.hash(allocator, input_slice[0 .. challenge_len + nonce_slice.len]) catch return false;
}
defer allocator.free(input);
var hex_encoder = utils.HexEncoder{};
switch (strategy) {
.leading_zeros => {
_ = hex_encoder.encode(input);
if (hex_encoder.countZeroes(difficulty)) {
return true;
}
},
.target_number => {
if (std.mem.eql(u8, hex_encoder.encode(input), target_slice.?)) {
return true;
}
},
else => unreachable,
}
return false;
} }
export fn validate_target_number_challenge(target_ptr: [*]u8, target_len: usize, nonce_ptr: [*]u8, nonce_len: usize, salt_ptr: [*]u8, salt_len: usize) i32 { fn validate_kctf(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, difficulty: usize) bool {
const target_slice = target_ptr[0..target_len]; const challenge_buf = challenge_ptr[0..challenge_len];
const salt_slice = salt_ptr[0..salt_len]; const solution_buf = solution_ptr[0..solution_len];
const nonce_slice = nonce_ptr[0..nonce_len];
const hash_hex_slice = hasher.hash(allocator, salt_slice, nonce_slice) catch return -2; const challenge = algorithms.kCTF.Challenge.from_string(allocator, challenge_buf, difficulty) catch return false;
const solution = algorithms.kCTF.Challenge.from_string(allocator, solution_buf, difficulty) catch return false;
if (!std.mem.eql(u8, target_slice, hash_hex_slice)) { defer {
return -3; challenge.destroy(allocator);
solution.destroy(allocator);
} }
return 0; const is_valid = challenge.verify(allocator, solution) catch return false;
return is_valid;
} }
export fn hash(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize) u64 { 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 challenge = challenge_ptr[0..challenge_len];
const nonce = nonce_ptr[0..nonce_len]; const nonce = nonce_ptr[0..nonce_len];
const hash_slice = hasher.hash(allocator, challenge, nonce) catch return 0;
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 => {
hash_slice = algorithms.Argon2.hash(allocator, challenge, nonce) catch return 0;
},
else => return 0,
}
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;
// bs to get the compiler to not whine about hash_slice.len being a u5 annd thus cannot be shifted by 32 // 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 = hash_slice.len; var ret: u64 = heap_hex_slice.len;
ret <<= 32; ret <<= 32;
ret |= @intFromPtr(hash_slice.ptr); ret |= @intFromPtr(heap_hex_slice.ptr);
allocator.free(hash_slice);
return ret; return ret;
} }
// pub fn main() void { pub fn main() anyerror!void {
// const challenge = "4d7220e22a1ea588fea60000ab8874194e4c6ffd71077adbae915826c73dbf48"; // TODO
// const nonce = "4302"; // if (comptime builtin.cpu.arch == .wasm32) return;
// const difficulty = 3;
// std.log.info("{d}", .{validate_challenge(@constCast(challenge[0..].ptr), challenge.len, @constCast(nonce[0..].ptr), nonce.len, difficulty)}); // const args = try std.process.argsAlloc(allocator);
// } // if (args.len < 3) {
// std.log.err("Usage: zig run src/validator.zig <challenge> <solution>", .{});
// 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});
}