Compare commits
3 Commits
trunk
...
benchmarki
| Author | SHA1 | Date | |
|---|---|---|---|
|
d0f4936b84
|
|||
|
e16383e9b9
|
|||
|
570531fe32
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
12
README.md
12
README.md
@@ -3,9 +3,9 @@
|
||||
Impost /ˈimˌpōst/ _noun_ a tax or compulsory payment
|
||||
|
||||
Impost is a PoW anti-spam solution, or for short, a PoW captcha. Instead of
|
||||
spying on your users and using heavy captchas, Impost uses PoW to impose a cost
|
||||
on sending requests. To a single user, this is a negligable few seconds, but at
|
||||
scale, it can be a significant deterrent to spam.
|
||||
spying on your users and using heavy, bloated captchas, Impost uses PoW to
|
||||
impose a cost on sending requests. To a single user, this is a negligable few
|
||||
seconds, but at scale, it can be a significant deterrent to spam.
|
||||
|
||||
This is the impost monorepo, containing the following packages:
|
||||
|
||||
@@ -14,6 +14,6 @@ This is the impost monorepo, containing the following packages:
|
||||
- `@impost/lib`: A library that can be used to generate, solve, and verify
|
||||
proofs.
|
||||
|
||||
It also contains a `solver` package, which is the Zig PoW solver that
|
||||
`@impost/lib` is built on top of, an example of how to use the solver in
|
||||
example-app
|
||||
It also contains a `solver` package, which is the PoW solver written in Zig,
|
||||
`@impost/lib` is built on top of, an example of how to use the solver in a
|
||||
nuxt 3 project. More in-depth documentation will be added in the future.
|
||||
|
||||
@@ -1 +1 @@
|
||||
IMPOST_HMAC_SECRET=xxx # openssl rand -base64 32
|
||||
YAPTCHA_HMAC_SECRET=xxx # openssl rand -base64 32
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# IMPOST
|
||||
# YAPTCHA
|
||||
|
||||
Yet Another Pow capTCHA.
|
||||
|
||||
## What is this
|
||||
|
||||
IMPOST is a proof of work based challenge-response system that is designed to
|
||||
YAPTCHA is a proof of work based challenge-response system that is designed to
|
||||
ward off spam and abuse.
|
||||
|
||||
<!-- TODO: -->
|
||||
|
||||
187
example-app/app/pages/bench.vue
Normal file
187
example-app/app/pages/bench.vue
Normal 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>
|
||||
@@ -9,7 +9,14 @@ if (!challengeData) {
|
||||
}
|
||||
|
||||
function solved(ev: CustomEvent) {
|
||||
console.log("Impost Solved:", ev.detail.solution);
|
||||
console.log("Solved:", ev.detail.solution);
|
||||
// $fetch('/api/pow/challenge', {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify({
|
||||
// challenge: ev.detail.challenge,
|
||||
// solution: ev.detail.solution,
|
||||
// }),
|
||||
// });
|
||||
}
|
||||
|
||||
function formsubmit(ev: Event) {
|
||||
@@ -19,9 +26,9 @@ function formsubmit(ev: Event) {
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center h-screen w-screen ">
|
||||
<form @submit="formsubmit" action="/"
|
||||
<form @submit.prevent="formsubmit"
|
||||
class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
|
||||
<impost-captcha name="impost" challengeUrl="/api/pow" auto="onsubmit"
|
||||
<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" />
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
algorithm = "argon2"
|
||||
strategy = "target_number"
|
||||
|
||||
[leading_zeroes]
|
||||
difficulty = 2
|
||||
difficulty = 4
|
||||
|
||||
[target_number]
|
||||
max_number = 192
|
||||
max_number = 10000
|
||||
|
||||
[kctf]
|
||||
difficulty = 100
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
},
|
||||
modules: ['@unocss/nuxt', ['nuxt-ssr-lit', { litElementPrefix: 'impost-' }]],
|
||||
modules: ['@unocss/nuxt', ['nuxt-ssr-lit', { litElementPrefix: 'pow-' }]],
|
||||
|
||||
nitro: {
|
||||
moduleSideEffects: ["@impost/widget"],
|
||||
@@ -28,9 +28,17 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
imports: {
|
||||
transform: {
|
||||
// only necessary in the monorepo since vite is going out to the
|
||||
// source of the widget and transforming it and clobbering it.
|
||||
exclude: [/impost/],
|
||||
}
|
||||
},
|
||||
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith('impost-'),
|
||||
isCustomElement: (tag) => tag.includes('pow-'),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
1456
example-app/package-lock.json
generated
1456
example-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"name": "hello-nuxt",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -9,13 +9,15 @@
|
||||
"preview": "nuxt preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@impost/lib": "workspace:*",
|
||||
"@impost/widget": "workspace:*",
|
||||
"@lit/reactive-element": "^2.1.1",
|
||||
"js-toml": "^1.0.2",
|
||||
"lit-element": "^4.2.1",
|
||||
"lit-html": "^3.3.1",
|
||||
"nuxt": "latest",
|
||||
"nuxt-ssr-lit": "1.6.32",
|
||||
"vue": "^3.5.25",
|
||||
"zod": "^4.1.12"
|
||||
"zod": "^4.1.12",
|
||||
"@impost/lib": "^0.1.0",
|
||||
"@impost/widget": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.0",
|
||||
|
||||
7
example-app/server/api/pow/algorithm.get.ts
Normal file
7
example-app/server/api/pow/algorithm.get.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
return {
|
||||
algorithm: config.algorithm
|
||||
}
|
||||
})
|
||||
43
example-app/server/api/pow/algorithm.put.ts
Normal file
43
example-app/server/api/pow/algorithm.put.ts
Normal 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}`
|
||||
};
|
||||
});
|
||||
@@ -1,25 +1,58 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { config } from '~~/server/utils/config';
|
||||
import { generate_challenge } from '@impost/lib/validator';
|
||||
import { ChallengeStrategy } from '@impost/lib';
|
||||
import { generate_challenge, kCTFChallengeConfig, Argon2ChallengeConfig, SHA256ChallengeConfig } from '@impost/lib/validator';
|
||||
import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
|
||||
import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow';
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
let challenge_config;
|
||||
switch (config.algorithm) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
switch (config.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
challenge_config = {
|
||||
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
||||
strategy: config.strategy,
|
||||
difficulty: config.leading_zeroes?.difficulty!,
|
||||
};
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
difficulty: config.leading_zeroes.difficulty,
|
||||
parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS },
|
||||
} as SHA256ChallengeConfig;
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
challenge_config = {
|
||||
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
||||
strategy: config.strategy,
|
||||
max_number: config.target_number.max_number,
|
||||
};
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
difficulty: config.target_number.max_number,
|
||||
parameters: { expires_at: Date.now() + CHALLENGE_TIMEOUT_MS },
|
||||
} as SHA256ChallengeConfig;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import * as z from 'zod';
|
||||
import { outstandingChallenges } from '~~/server/utils/pow';
|
||||
|
||||
const challengeSchema = z.object({
|
||||
challenge: z.string(),
|
||||
nonce: z.string()
|
||||
salt: 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
|
||||
@@ -13,29 +14,32 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readValidatedBody(event, challengeSchema.safeParse);
|
||||
|
||||
if (!body.success) {
|
||||
const errors = z.treeifyError(body.error);
|
||||
const error_message = Object.entries(errors.errors).map(([key, value]) => `${key}: ${value}`).join('\n');
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: error_message
|
||||
statusMessage: 'Validation failed'
|
||||
})
|
||||
}
|
||||
|
||||
let { challenge, nonce } = body.data;
|
||||
let { salt, solution } = body.data;
|
||||
|
||||
const outstanding_challenge = outstandingChallenges.get(body.data.challenge)!;
|
||||
// always delete the challenge on a solve attempt
|
||||
clearTimeout(outstanding_challenge.timeout);
|
||||
outstandingChallenges.delete(challenge);
|
||||
const outstanding_challenge = outstandingChallenges.get(salt);
|
||||
if (outstanding_challenge === undefined) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Challenge not found'
|
||||
})
|
||||
}
|
||||
|
||||
// check if the challenge is valid
|
||||
let challenge_valid = await validate_challenge(outstandingChallenges.get(challenge)!.challenge, {
|
||||
challenge,
|
||||
nonce,
|
||||
});
|
||||
const challenge_valid = await validate_challenge(outstanding_challenge.challenge, solution);
|
||||
|
||||
console.log("CHALLENGE VALID", challenge_valid);
|
||||
|
||||
if (challenge_valid) {
|
||||
// clear the challenge
|
||||
clearTimeout(outstandingChallenges.get(salt)!.timeout);
|
||||
outstandingChallenges.delete(salt);
|
||||
|
||||
return {
|
||||
message: 'Challenge solved'
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { ChallengeStrategy } from '@impost/lib';
|
||||
import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
let difficulty: number;
|
||||
|
||||
console.log("CONFIG", config);
|
||||
|
||||
switch (config.algorithm) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
case ChallengeAlgorithm.Argon2:
|
||||
switch (config.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
difficulty = config.leading_zeroes.difficulty!;
|
||||
@@ -12,6 +17,11 @@ export default defineEventHandler((event) => {
|
||||
difficulty = config.target_number.max_number!;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.kCTF:
|
||||
difficulty = config.kctf.difficulty!;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
difficulty
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { ChallengeStrategy } from '@impost/lib';
|
||||
import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
|
||||
let difficulty = body.difficulty;
|
||||
|
||||
switch (config.algorithm) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
case ChallengeAlgorithm.Argon2:
|
||||
switch (config.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
if (!difficulty || difficulty < 1 || difficulty > 64) {
|
||||
@@ -28,8 +31,13 @@ export default defineEventHandler(async (event) => {
|
||||
config.target_number.max_number = difficulty;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.kCTF:
|
||||
config.kctf.difficulty = difficulty;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Challenge difficulty set'
|
||||
message: `Challenge difficulty set to ${difficulty}`
|
||||
};
|
||||
});
|
||||
35
example-app/server/api/pow/index.get.ts
Normal file
35
example-app/server/api/pow/index.get.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
14
example-app/server/api/pow/strategy.get.ts
Normal file
14
example-app/server/api/pow/strategy.get.ts
Normal 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,
|
||||
}
|
||||
});
|
||||
43
example-app/server/api/pow/strategy.put.ts
Normal file
43
example-app/server/api/pow/strategy.put.ts
Normal 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}`
|
||||
};
|
||||
});
|
||||
@@ -1,30 +1,53 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { load } from 'js-toml';
|
||||
import z from 'zod';
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
const LeadingZeroesSchema = z.object({
|
||||
const SHA256Schema = z.discriminatedUnion("strategy", [
|
||||
z.object({
|
||||
algorithm: z.literal(ChallengeAlgorithm.SHA256),
|
||||
strategy: z.literal(ChallengeStrategy.LeadingZeroes),
|
||||
leading_zeroes: z.object({
|
||||
difficulty: z.number().int().min(1).max(64),
|
||||
}),
|
||||
});
|
||||
|
||||
const TargetNumberSchema = z.object({
|
||||
}),
|
||||
z.object({
|
||||
algorithm: z.literal(ChallengeAlgorithm.SHA256),
|
||||
strategy: z.literal(ChallengeStrategy.TargetNumber),
|
||||
target_number: z.object({
|
||||
max_number: z.number().int().min(1).max(100_000),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
const 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 const Config = z.discriminatedUnion('strategy', [
|
||||
LeadingZeroesSchema,
|
||||
TargetNumberSchema,
|
||||
]);
|
||||
|
||||
export let config: Config;
|
||||
|
||||
try {
|
||||
|
||||
30
justfile
Normal file
30
justfile
Normal file
@@ -0,0 +1,30 @@
|
||||
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz"
|
||||
zig-build-args := "--release=fast -Dtarget=wasm32-freestanding -Dcpu=generic+bulk_memory+bulk_memory_opt+simd128+tail_call"
|
||||
|
||||
npm-runner := "npm"
|
||||
|
||||
[working-directory: "example-app"]
|
||||
playground: build
|
||||
{{npm-runner}} run dev
|
||||
|
||||
build: build-widget
|
||||
|
||||
[working-directory: "solver"]
|
||||
build-wasm:
|
||||
zig build {{zig-build-args}}
|
||||
wasm-opt {{wasm-opt-args}} --enable-simd -o zig-out/bin/solver.wasm zig-out/bin/solver.wasm
|
||||
# The server does not support simd, so we disable it here
|
||||
wasm-opt {{wasm-opt-args}} --disable-simd -o zig-out/bin/validator.wasm zig-out/bin/validator.wasm
|
||||
|
||||
[working-directory: "packages/lib"]
|
||||
build-lib: build-wasm
|
||||
{{npm-runner}} install
|
||||
{{npm-runner}} link
|
||||
{{npm-runner}} run build
|
||||
|
||||
[working-directory: "packages/widget"]
|
||||
build-widget: build-lib
|
||||
{{npm-runner}} install
|
||||
{{npm-runner}} link @impost/lib
|
||||
{{npm-runner}} link
|
||||
{{npm-runner}} run build
|
||||
17
package.json
17
package.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "impost-monorepo",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm build:all",
|
||||
"build:all": "pnpm build:solver && pnpm build:lib && pnpm build:widget",
|
||||
"build:solver": "pnpm --filter solver build",
|
||||
"build:lib": "pnpm --filter lib build",
|
||||
"build:widget": "pnpm --filter widget build",
|
||||
"dev:example": "pnpm --filter example-app dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.21.0+sha512.da3337267e400fdd3d479a6c68079ac6db01d8ca4f67572083e722775a796788a7a9956613749e000fac20d424b594f7a791a5f4e2e13581c5ef947f26968a40"
|
||||
}
|
||||
@@ -1,16 +1,43 @@
|
||||
import { UUID } from "uuidv7";
|
||||
|
||||
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 {
|
||||
Null = "null",
|
||||
LeadingZeroes = "leading_zeroes",
|
||||
TargetNumber = "target_number",
|
||||
}
|
||||
|
||||
export function strategyToInt(strategy: ChallengeStrategy): number {
|
||||
switch (strategy) {
|
||||
case ChallengeStrategy.Null:
|
||||
return 0;
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
return 1;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// In this case, the client will repeatedly hash a number with has until it
|
||||
// finds a hash thaat starts with *difficulty* leading zeroes
|
||||
export interface ChallengeLeadingZeroes {
|
||||
algorithm: ChallengeAlgorithm;
|
||||
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string; // random string
|
||||
difficulty: number;
|
||||
@@ -19,10 +46,24 @@ export interface ChallengeLeadingZeroes {
|
||||
// In this case, the server generates a random number, and the client will hash
|
||||
// the salt (a random string) + a random number until it finds a hash that is equal to challenge
|
||||
export interface ChallengeTargetNumber {
|
||||
algorithm: ChallengeAlgorithm;
|
||||
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
salt: string; // random string
|
||||
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;
|
||||
@@ -1,11 +1,10 @@
|
||||
import WASMSolverUrl from '../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
import { ChallengeStrategy, type Challenge, type ChallengeLeadingZeroes, type ChallengeTargetNumber } from '.';
|
||||
import { ChallengeAlgorithm, ChallengeStrategy, algorithmToInt, strategyToInt } from "./index";
|
||||
import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
"free": (ptr: number | null, byte_count: number) => void;
|
||||
"solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number;
|
||||
"solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: number) => number;
|
||||
"solve": (algorithm: number, strategy: number, salt_ptr: number, salt_len: number, difficulty: number, target_ptr: number, target_len: number) => number,
|
||||
"memory": WebAssembly.Memory;
|
||||
}
|
||||
|
||||
@@ -31,75 +30,100 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P
|
||||
}) as unknown as SolverModule;
|
||||
}
|
||||
|
||||
export function solve_challenge(solver: SolverModule, challenge: Challenge): number {
|
||||
switch (challenge.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
return solve_leaading_zeroes_challenge(solver, challenge);
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
return solve_target_number_challenge(solver, challenge);
|
||||
default:
|
||||
throw new Error("Invalid challenge strategy");
|
||||
type Argon2LeadingZeroesParams = {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: ChallengeLeadingZeroes): number {
|
||||
const { salt, difficulty } = challenge;
|
||||
const encoder = new TextEncoder();
|
||||
let salt_buf = encoder.encode(algorithm.salt);
|
||||
|
||||
const salt_bytes = encoder.encode(salt);
|
||||
|
||||
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||
let salt_ptr = solver.exports.malloc(salt_buf.length);
|
||||
if (salt_ptr === 0 || salt_ptr === null) {
|
||||
throw new Error("Failed to allocate memory for challenge string");
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
let memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_buf, salt_ptr);
|
||||
|
||||
const ret = solver.exports.solve_leaading_zeroes_challenge(
|
||||
salt_ptr,
|
||||
salt_bytes.length,
|
||||
difficulty,
|
||||
);
|
||||
|
||||
if (ret < 0) {
|
||||
throw new Error("Failed to solve challenge");
|
||||
let ret: string | number;
|
||||
switch (algorithm.name) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
case ChallengeAlgorithm.Argon2:
|
||||
switch (algorithm.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes: {
|
||||
ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.LeadingZeroes), salt_ptr, salt_buf.length, algorithm.difficulty, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function solve_target_number_challenge(solver: SolverModule, challenge: ChallengeTargetNumber): 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);
|
||||
case ChallengeStrategy.TargetNumber: {
|
||||
const target_buf = encoder.encode(algorithm.target);
|
||||
const target_ptr = solver.exports.malloc(target_buf.length);
|
||||
if (target_ptr === 0 || target_ptr === null) {
|
||||
throw new Error("Failed to allocate memory for target string");
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(target_buf, target_ptr);
|
||||
|
||||
const ret = solver.exports.solve_target_number_challenge(
|
||||
target_ptr,
|
||||
target_bytes.length,
|
||||
salt_ptr,
|
||||
salt_bytes.length,
|
||||
);
|
||||
ret = solver.exports.solve(algorithmToInt(algorithm.name), strategyToInt(ChallengeStrategy.TargetNumber), salt_ptr, salt_buf.length, 0, target_ptr, target_buf.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.kCTF:
|
||||
const solution_ptr = solver.exports.solve(algorithmToInt(ChallengeAlgorithm.kCTF), strategyToInt(ChallengeStrategy.Null), salt_ptr, salt_buf.length, algorithm.difficulty, 0, 0);
|
||||
|
||||
if (ret < 0) {
|
||||
if (solution_ptr <= 0) {
|
||||
throw new Error("Failed to solve challenge");
|
||||
}
|
||||
|
||||
const length = new DataView(solver.exports.memory.buffer, solution_ptr, 2).getUint16(0, true);
|
||||
ret = new TextDecoder().decode(solver.exports.memory.buffer.slice(solution_ptr + 2, solution_ptr + 2 + length));
|
||||
|
||||
solver.exports.free(solution_ptr, 2 + length);
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge, type ChallengeLeadingZeroes, type ChallengeTargetNumber } from '.';
|
||||
import WASMValidatorUrl from '../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||
import { ChallengeStrategy, type Challenge, type InnerChallenge, ChallengeAlgorithm, algorithmToInt, strategyToInt } from '.';
|
||||
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||
import { uuidv7obj } from 'uuidv7';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
"free": (ptr: number | null, byte_count: number) => void;
|
||||
"validate_leading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number, difficulty: number) => number;
|
||||
"validate_target_number_challenge": (target_ptr: number, target_len: number, nonce_ptr: number, nonce_len: number, salt_ptr: number, salt_len: number) => number;
|
||||
"hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint;
|
||||
"validate": (algorithm: number, strategy: number, challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number, nonce: number, difficulty: number) => boolean;
|
||||
"hash": (challenge_ptr: number, challebge_len: number, nonce_ptr: number, nonce_len: number, algorithm: number) => bigint;
|
||||
"memory": WebAssembly.Memory;
|
||||
}
|
||||
|
||||
@@ -14,159 +14,281 @@ export interface ValidatorModule extends WebAssembly.Instance {
|
||||
exports: WasmExports;
|
||||
}
|
||||
|
||||
function array_to_base64(buffer: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
function array_to_base64(buffer: ArrayBufferLike): string {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (var i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<ChallengeLeadingZeroes> {
|
||||
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
|
||||
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
|
||||
export interface SHA256ChallengeConfig {
|
||||
algorithm: ChallengeAlgorithm.SHA256;
|
||||
strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber;
|
||||
difficulty: number;
|
||||
parameters: Object;
|
||||
}
|
||||
|
||||
let challenge: ChallengeLeadingZeroes = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt,
|
||||
difficulty,
|
||||
};
|
||||
export interface Argon2ChallengeConfig {
|
||||
algorithm: ChallengeAlgorithm.Argon2;
|
||||
strategy: ChallengeStrategy.LeadingZeroes | ChallengeStrategy.TargetNumber;
|
||||
difficulty: number;
|
||||
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;
|
||||
}
|
||||
|
||||
async function generate_target_number_challenge(parameters: Object, max_number: number): Promise<ChallengeTargetNumber | null> {
|
||||
// in target number config, since we need to generate a target hash, we
|
||||
// need to hash the salt + nonce, so the client knows what the target is
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule;
|
||||
export async function generate_challenge(config: ChallengeConfig): Promise<Challenge | null> {
|
||||
if (config.difficulty < 1) {
|
||||
throw new Error("Difficulty must be at least 1");
|
||||
}
|
||||
|
||||
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
|
||||
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
|
||||
let random_number = new DataView(crypto.getRandomValues(new Uint8Array(4)).buffer).getUint32(0, true) % max_number;
|
||||
const 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 salt_bytes = encoder.encode(salt);
|
||||
const random_number_bytes = encoder.encode(random_number.toString());
|
||||
|
||||
const salt_ptr = validator.exports.malloc(salt_bytes.length);
|
||||
const random_number_ptr = validator.exports.malloc(random_number_bytes.length);
|
||||
var inner_challenge: InnerChallenge = {
|
||||
algorithm: config.algorithm,
|
||||
} as InnerChallenge;
|
||||
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}`;
|
||||
let parameters_str: string;
|
||||
switch (config.algorithm) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
case ChallengeAlgorithm.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");
|
||||
return null;
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(random_number_bytes, random_number_ptr);
|
||||
memory.set(challenge_buf, challenge_ptr);
|
||||
|
||||
let target_blob: bigint = validator.exports.hash(salt_ptr, salt_bytes.length, random_number_ptr, random_number_bytes.length);
|
||||
let target_ptr = Number(target_blob & BigInt(0xFFFFFFFF));
|
||||
let target_len = Number(target_blob >> BigInt(32));
|
||||
const challenge_len = inner_challenge.salt.length;
|
||||
const nonce_ptr = challenge_ptr + challenge_len;
|
||||
const nonce_len = challenge_buf.length - challenge_len;
|
||||
|
||||
validator.exports.free(salt_ptr, salt_bytes.length);
|
||||
validator.exports.free(random_number_ptr, random_number_bytes.length);
|
||||
const target = validator.exports.hash(challenge_ptr, challenge_len, nonce_ptr, nonce_len, algorithmToInt(inner_challenge.algorithm));
|
||||
|
||||
// do NOT use `memory` here, by this time it has almost definitely been resized and will cause errors to touch
|
||||
let target_slice = new Uint8Array(validator.exports.memory.buffer.slice(target_ptr, target_ptr + target_len));
|
||||
const target = new TextDecoder().decode(target_slice);
|
||||
const target_len = Number((target >> 32n) & 0xFFFFFFFFn);
|
||||
const target_ptr = Number(target & 0xFFFFFFFFn);
|
||||
|
||||
let challenge: ChallengeTargetNumber = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
salt,
|
||||
target
|
||||
};
|
||||
const target_buf = new Uint8Array(validator.exports.memory.buffer, target_ptr, target_len);
|
||||
// @ts-ignore
|
||||
inner_challenge.target = new TextDecoder().decode(target_buf);
|
||||
// @ts-ignore
|
||||
console.log("TARGET", inner_challenge.target);
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
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);
|
||||
validator.exports.free(challenge_ptr, challenge_len + random_number.length);
|
||||
validator.exports.free(target_ptr, target_len);
|
||||
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;
|
||||
}
|
||||
|
||||
if (challenge === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return challenge;
|
||||
return await encode_challenge(inner_challenge, config.parameters);
|
||||
}
|
||||
|
||||
export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise<boolean> {
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
|
||||
|
||||
export async function validate_challenge(challenge: Challenge, challenge_solution: string | number): Promise<boolean> {
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl), {
|
||||
env: {
|
||||
__log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(validator.exports.memory.buffer, str_ptr, str_len))),
|
||||
}
|
||||
})).instance as unknown as ValidatorModule
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
let err;
|
||||
let memory;
|
||||
let nonce_bytes, nonce_ptr;
|
||||
let target_bytes, target_ptr;
|
||||
if (challenge.algorithm === ChallengeAlgorithm.kCTF) {
|
||||
challenge.salt = challenge.salt.split("?")[0];
|
||||
}
|
||||
|
||||
const challenge_buf = encoder.encode(challenge.salt);
|
||||
const challenge_ptr = validator.exports.malloc(challenge_buf.length);
|
||||
|
||||
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) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
target_bytes = encoder.encode(challenge_solution.challenge);
|
||||
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;
|
||||
return validator.exports.validate(algorithmToInt(challenge.algorithm), strategyToInt(challenge.strategy), challenge_ptr, challenge_buf.length, 0, 0, challenge_solution, challenge.difficulty);
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
target_bytes = encoder.encode(challenge.target);
|
||||
const salt_bytes = encoder.encode(challenge.salt);
|
||||
nonce_bytes = encoder.encode(challenge_solution.nonce);
|
||||
|
||||
const salt_ptr = validator.exports.malloc(salt_bytes.length);
|
||||
target_ptr = validator.exports.malloc(target_bytes.length);
|
||||
nonce_ptr = validator.exports.malloc(nonce_bytes.length);
|
||||
|
||||
if (salt_ptr === 0 || salt_ptr === null || target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
|
||||
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;
|
||||
}
|
||||
|
||||
memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
memory.set(nonce_bytes, nonce_ptr);
|
||||
const memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(solution_buf, solution_ptr);
|
||||
|
||||
err = validator.exports.validate_target_number_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, salt_ptr, salt_bytes.length);
|
||||
|
||||
validator.exports.free(salt_ptr, salt_bytes.length);
|
||||
validator.exports.free(target_ptr, target_bytes.length);
|
||||
validator.exports.free(nonce_ptr, nonce_bytes.length);
|
||||
break;
|
||||
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.Argon2:
|
||||
if (typeof challenge_solution === "string") {
|
||||
throw new Error("Argon2 challenges do not support a solution as a number");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default defineConfig({
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
minify: true
|
||||
sourcemap: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "solver",
|
||||
"description": "Zig WASM POW solver, not an actual node package, just using this so that pnpm will build it for me",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm run build:wasm && pnpm run minify",
|
||||
"build:wasm": "zig build --release=fast -Dtarget=wasm32-freestanding -Dcpu=generic+bulk_memory+bulk_memory_opt+simd128+tail_call",
|
||||
"minify": "wasm-opt --strip-debug --strip-dwarf -O4 -o zig-out/bin/solver.wasm zig-out/bin/solver.wasm && wasm-opt --strip-debug --strip-dwarf -O4 -o zig-out/bin/validator.wasm zig-out/bin/validator.wasm"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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 (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;
|
||||
|
||||
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);
|
||||
|
||||
return derived[0..];
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
const argon2 = @import("argon2.zig");
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
extern fn __get_solution() i32;
|
||||
extern fn __set_solution(value: i32) void;
|
||||
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
|
||||
extern fn __fetch_add_nonce(value: i32) i32;
|
||||
extern fn __log(str_ptr: usize, str_len: usize) void;
|
||||
|
||||
fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
|
||||
if (comptime builtin.target.cpu.arch != .wasm32) {
|
||||
std.log.defaultLog(level, scope, fmt, args);
|
||||
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 };
|
||||
var hex_encoder = utils.HexEncoder{};
|
||||
|
||||
export fn malloc(byte_count: usize) ?*u8 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
return @ptrCast(ptr.ptr);
|
||||
}
|
||||
|
||||
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
if (ptr) |p| {
|
||||
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||
allocator.free(cast_ptr[0..byte_count]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||
|
||||
if (difficulty < 1 or difficulty > 64) {
|
||||
std.log.err("Invalid difficulty for leading zeroes\n", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
const max_nonce_iterations: u64 = 1_000_000_000;
|
||||
|
||||
// 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 {
|
||||
std.log.err("Failed to allocate memory for challenge\n", .{});
|
||||
return -1;
|
||||
};
|
||||
// dont leak memory :pepega:
|
||||
defer allocator.free(input_buffer);
|
||||
|
||||
@memcpy(input_buffer[0..challenge_len], challenge_slice);
|
||||
|
||||
var nonce = __fetch_add_nonce(1);
|
||||
|
||||
while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
|
||||
if (__get_solution() != -1) {
|
||||
// solution has already been found, no point in continuing
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nonce_str = std.fmt.bufPrint(input_buffer[challenge_len..], "{d}", .{nonce}) catch {
|
||||
std.log.err("Failed to allocate memory for nonce\n", .{});
|
||||
return -1;
|
||||
};
|
||||
|
||||
const argon2_key = argon2.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
|
||||
std.log.err("Failed to hash argon2 key\n", .{});
|
||||
return -1;
|
||||
};
|
||||
|
||||
_ = hex_encoder.encode(argon2_key);
|
||||
if (!hex_encoder.countZeroes(difficulty)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, salt_ptr: [*]u8, salt_len: usize) i32 {
|
||||
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 {
|
||||
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 {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const argon2_key = argon2.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
if (std.mem.eql(u8, target_slice, hex_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const argon2 = @import("argon2.zig");
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
var hex_encoder = utils.HexEncoder{};
|
||||
|
||||
export fn malloc(byte_count: usize) ?*u8 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
return @ptrCast(ptr.ptr);
|
||||
}
|
||||
|
||||
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
if (ptr) |p| {
|
||||
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||
allocator.free(cast_ptr[0..byte_count]);
|
||||
}
|
||||
}
|
||||
|
||||
export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, difficulty: u32) i32 {
|
||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||
const nonce_slice = nonce_ptr[0..nonce_len];
|
||||
|
||||
if (difficulty < 1 or difficulty > 64) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const argon2_key = argon2.hash(allocator, challenge_slice, nonce_slice) catch return -2;
|
||||
|
||||
_ = hex_encoder.encode(argon2_key);
|
||||
if (!hex_encoder.countZeroes(difficulty)) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 {
|
||||
const target_slice = target_ptr[0..target_len];
|
||||
const salt_slice = salt_ptr[0..salt_len];
|
||||
const nonce_slice = nonce_ptr[0..nonce_len];
|
||||
|
||||
const argon2_key = argon2.hash(allocator, salt_slice, nonce_slice) catch return -2;
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
if (!std.mem.eql(u8, target_slice, hex_slice)) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export fn hash(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize) u64 {
|
||||
const challenge = challenge_ptr[0..challenge_len];
|
||||
const nonce = nonce_ptr[0..nonce_len];
|
||||
const argon2_key = argon2.hash(allocator, challenge, nonce) catch return 0;
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
// 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 = hex_slice.len;
|
||||
ret <<= 32;
|
||||
ret |= @intFromPtr(hex_slice.ptr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// pub fn main() void {
|
||||
// const challenge = "4d7220e22a1ea588fea60000ab8874194e4c6ffd71077adbae915826c73dbf48";
|
||||
// const nonce = "4302";
|
||||
// const difficulty = 3;
|
||||
|
||||
// std.log.info("{d}", .{validate_challenge(@constCast(challenge[0..].ptr), challenge.len, @constCast(nonce[0..].ptr), nonce.len, difficulty)});
|
||||
// }
|
||||
8
packages/widget/package-lock.json
generated
8
packages/widget/package-lock.json
generated
@@ -8,9 +8,6 @@
|
||||
"name": "@impost/widget",
|
||||
"version": "0.1.0",
|
||||
"license": "BSL-1.0",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
@@ -1657,11 +1654,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"lit-element": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@impost/lib": "workspace:*",
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
"lit-element": "^3.1.2",
|
||||
@@ -31,7 +30,6 @@
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"comlink": "^4.4.2"
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { type Challenge } from '@impost/lib';
|
||||
import { get_wasm_module } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
import SolverWorker from './solver-worker?worker&inline';
|
||||
|
||||
type SolverWorkerAPI = Comlink.Remote<typeof import("./solver-worker")>;
|
||||
|
||||
@customElement('impost-captcha')
|
||||
export class ImpostCaptcha extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: var(--impost-widget-width, 250px);
|
||||
}
|
||||
.impost-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--impost-widget-border-color, #505050);
|
||||
font-family: sans-serif;
|
||||
background-color: var(--impost-widget-background-color, #070408);
|
||||
border-radius: var(--impost-widget-border-radius, 8px);
|
||||
}
|
||||
.impost-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.impost-main span {
|
||||
height: min-content;
|
||||
}
|
||||
.impost-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: var(--impost-widget-checkbox-color, #6e6e6eff);
|
||||
}
|
||||
.impost-checkbox input {
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
.impost-footer {
|
||||
display: flex;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||
justify-content: space-around;
|
||||
align-items: end;
|
||||
}
|
||||
.impost-footer div {
|
||||
height: min-content;
|
||||
}
|
||||
.impost-footer #provider-link {
|
||||
margin-left: auto;
|
||||
}
|
||||
.impost-footer div a {
|
||||
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||
}
|
||||
.impost-footer div a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
/// ================================================
|
||||
/// Configuration
|
||||
/// ================================================
|
||||
|
||||
|
||||
@property({ type: String })
|
||||
auto: "onload" | "onfocus" | "onsubmit" | "off" = "off";
|
||||
|
||||
@property({ type: String })
|
||||
challengeUrl: string = 'http://localhost:3000/api/pow';
|
||||
|
||||
@property({ type: String })
|
||||
challengejson: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
showHashrate: boolean = false;
|
||||
|
||||
/// ================================================
|
||||
/// Internals
|
||||
/// ================================================
|
||||
|
||||
// needed to allow for multiple widgets on the same page if you wanted to
|
||||
// do that for some reason
|
||||
private uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
|
||||
private _internals: ElementInternals | null = null;
|
||||
static formAssociated = true;
|
||||
|
||||
@state()
|
||||
private solution: string | null = null;
|
||||
|
||||
@state()
|
||||
private challengeData: Challenge | null = null;
|
||||
|
||||
@state()
|
||||
private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
|
||||
|
||||
@state()
|
||||
private disabled: boolean = true;
|
||||
|
||||
@state()
|
||||
private hashRate: number = 0;
|
||||
|
||||
|
||||
// stores the nonce and solution atomics
|
||||
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||
|
||||
private solverWorkers: SolverWorkerAPI[] | null = null;
|
||||
private nativeWorkers: Worker[] | null = null;
|
||||
|
||||
private solveStartTime: number | null = null;
|
||||
private hashRateInterval: number | null = null;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._internals = this.attachInternals();
|
||||
this.fetchChallenge();
|
||||
|
||||
console.log(this._internals.form);
|
||||
|
||||
this.initWorkers();
|
||||
|
||||
switch (this.auto) {
|
||||
case 'onload':
|
||||
this.solveChallenge();
|
||||
break;
|
||||
case 'onfocus':
|
||||
this.addEventListener('focus', () => this.solveChallenge());
|
||||
break;
|
||||
case 'onsubmit':
|
||||
if (this.parentElement?.nodeName === 'FORM') {
|
||||
this.parentElement.addEventListener('submit', async (ev) => {
|
||||
if (this.status === 'solved') {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
await this.solveChallenge();
|
||||
|
||||
const form = this.parentElement as HTMLFormElement;
|
||||
|
||||
if (form.requestSubmit) {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
|
||||
for (const worker of this.nativeWorkers || []) {
|
||||
worker.terminate();
|
||||
this.solverWorkers = null;
|
||||
this.nativeWorkers = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentWorkingNonce() {
|
||||
return Atomics.load(new Uint32Array(this.sab), 0);
|
||||
}
|
||||
|
||||
async fetchChallenge() {
|
||||
if (this.challengeData !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.challengejson !== '' && this.challengeData === null) {
|
||||
this.challengeData = JSON.parse(this.challengejson);
|
||||
this.challengejson = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// challenge data must be provided by the user when using SSR
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`${this.challengeUrl}/challenge`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.challengeData = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching challenge:', error);
|
||||
this.status = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
this.solverWorkers = [];
|
||||
this.nativeWorkers = [];
|
||||
|
||||
const num_workers = navigator.hardwareConcurrency;
|
||||
for (let i = 0; i < num_workers; i++) {
|
||||
const nativeWorker = new SolverWorker();
|
||||
const comlinkWorker = Comlink.wrap<SolverWorkerAPI>(nativeWorker);
|
||||
|
||||
this.nativeWorkers.push(nativeWorker);
|
||||
this.solverWorkers.push(comlinkWorker);
|
||||
}
|
||||
|
||||
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 worker_promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < this.solverWorkers.length; i++) {
|
||||
const solver = this.solverWorkers[i]!;
|
||||
worker_promises.push(solver.init(wasm_module, this.sab)); // Direct call to exposed `init` method
|
||||
}
|
||||
|
||||
const timeoutMs = 10 * 1000;
|
||||
let timeout: number;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
reject(new Error(`Function timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
Promise.allSettled(worker_promises).then(results => {
|
||||
const failedInits = results.filter(r => r.status === 'rejected');
|
||||
if (failedInits.length > 0) {
|
||||
console.error('Some workers failed to initialize:', failedInits);
|
||||
// we might want to collect all errors, and if every
|
||||
// worker fails, we can throw, but carry on if only some
|
||||
// fail. For now, we'll just throw if any fail.
|
||||
|
||||
throw new Error("One or more workers failed to initialize.");
|
||||
}
|
||||
|
||||
console.log('All workers initialized');
|
||||
return;
|
||||
}),
|
||||
timeoutPromise,
|
||||
]).then(() => {
|
||||
clearTimeout(timeout);
|
||||
}).catch(error => {
|
||||
clearTimeout(timeout);
|
||||
console.error("Worker initialization failed:", error);
|
||||
this.status = 'error';
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async solveChallenge() {
|
||||
if (!this.challengeData || this.solverWorkers === null) {
|
||||
// in all normal cases, this should be impossible
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.solution !== null || this.status !== 'unsolved') {
|
||||
// do not solve twice
|
||||
return;
|
||||
}
|
||||
|
||||
this.solveStartTime = performance.now();
|
||||
this.hashRateInterval = setInterval(async () => {
|
||||
const nonce = this.getCurrentWorkingNonce();
|
||||
|
||||
this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000));
|
||||
console.log(this.hashRate);
|
||||
}, 250);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||
detail: { challenge: this.challengeData, }, // empty solution
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
|
||||
this.status = 'solving';
|
||||
this.solution = null;
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
// reset atomics
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, -1);
|
||||
|
||||
console.log('Sending challenge to workers...');
|
||||
// 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
|
||||
// unblock, they return 0 since the challenge is already solved.
|
||||
//
|
||||
// We need to do a better job of tracking solvers, so if one worker
|
||||
// errors out, we only error out if all workers have errored out.
|
||||
let worker_promises: Promise<string>[] = [];
|
||||
for (let solver of this.solverWorkers) {
|
||||
worker_promises.push(solver.solve_challenge(this.challengeData as Challenge));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.race(worker_promises);
|
||||
|
||||
// The true solution is stored in the SharedArrayBuffer.
|
||||
this.solution = String(Atomics.load(atomics_view, 1));
|
||||
this.status = 'solved';
|
||||
} catch (error: any) {
|
||||
console.error("Captcha solving failed:", error);
|
||||
this.status = 'error';
|
||||
} finally {
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.status === 'solved') {
|
||||
this._internals!.setFormValue(JSON.stringify({
|
||||
challenge: this.challengeData.salt,
|
||||
solution: this.solution,
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
challenge: this.challengeData,
|
||||
solution: this.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
solvePreventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.solveChallenge();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.challengejson !== '' && this.challengeData === null) {
|
||||
this.challengeData = JSON.parse(this.challengejson);
|
||||
this.challengejson = '';
|
||||
}
|
||||
|
||||
if (this.challengeData === null) {
|
||||
return html`
|
||||
<div class="loading-message">Loading captcha challenge...</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="impost-widget">
|
||||
<div class="impost-main">
|
||||
<div class="impost-checkbox">
|
||||
${this.status !== 'solving' ? html`${this.status === 'error' ? html`<svg class="impost-error-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 14q-.425 0-.712-.288T11 13V6q0-.425.288-.712T12 5t.713.288T13 6v7q0 .425-.288.713T12 14m0 5q-.425 0-.712-.288T11 18t.288-.712T12 17t.713.288T13 18t-.288.713T12 19"/></svg>` : html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||
`}` : html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||
<g stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="9.5" fill="none" stroke-linecap="round" stroke-width="3">
|
||||
<animate attributeName="stroke-dasharray" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0 150;42 150;42 150;42 150"/>
|
||||
<animate attributeName="stroke-dashoffset" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0;-16;-59;-59"/>
|
||||
</circle>
|
||||
<animateTransform attributeName="transform" dur="2s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
|
||||
</g>
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<label for="impost-checkbox-${this.uid}">${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
</div>
|
||||
|
||||
<div class="impost-footer">
|
||||
${this.showHashrate && this.status === 'solving' && this.hashRate > 0 ? html`
|
||||
<div>
|
||||
<span>H/s:</span>
|
||||
<span>${this.hashRate.toFixed(2)}</span>
|
||||
</div>
|
||||
` : ``}
|
||||
<div id="provider-link">
|
||||
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type = "text"
|
||||
name = "impost-solution"
|
||||
id = "impost-solution-${this.uid}"
|
||||
style="display: none;"
|
||||
.value = ${this.solution}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from './captcha';
|
||||
export * from './pow-captcha';
|
||||
534
packages/widget/src/pow-captcha.ts
Normal file
534
packages/widget/src/pow-captcha.ts
Normal file
@@ -0,0 +1,534 @@
|
||||
import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker';
|
||||
import { type Challenge, ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
|
||||
import { get_wasm_module } from '@impost/lib/solver';
|
||||
|
||||
import ChallengeWorker from './solver-worker?worker&inline';
|
||||
|
||||
@customElement('pow-captcha')
|
||||
export class PowCaptcha extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: var(--impost-widget-width, 250px);
|
||||
}
|
||||
.impost-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--impost-widget-border-color, #505050);
|
||||
font-family: sans-serif;
|
||||
background-color: var(--impost-widget-background-color, #070408);
|
||||
border-radius: var(--impost-widget-border-radius, 8px);
|
||||
}
|
||||
.impost-error-icon {
|
||||
color: var(--impost-widget-error-icon-color, #FF8117);
|
||||
}
|
||||
.impost-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.impost-main span {
|
||||
height: min-content;
|
||||
}
|
||||
.impost-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: var(--impost-widget-checkbox-color, #6e6e6eff);
|
||||
}
|
||||
.impost-checkbox input {
|
||||
height: 80%;
|
||||
width: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
.impost-footer {
|
||||
display: flex;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||
justify-content: space-around;
|
||||
align-items: end;
|
||||
}
|
||||
.impost-footer div {
|
||||
height: min-content;
|
||||
}
|
||||
.impost-footer #provider-link {
|
||||
margin-left: auto;
|
||||
}
|
||||
.impost-footer div a {
|
||||
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||
}
|
||||
.impost-footer div a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// one of: "load", "focus", "submit", "off"
|
||||
@property({ type: String })
|
||||
auto: "onload" | "onfocus" | "onsubmit" | "off" = "off";
|
||||
|
||||
@property({ type: String })
|
||||
challengeUrl: string = 'http://localhost:3000/api/pow';
|
||||
|
||||
@property({ type: String })
|
||||
challengejson: string = '';
|
||||
|
||||
// needed to allow for multiple widgets on the same page if you wanted to
|
||||
// do that for some reason
|
||||
uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
|
||||
@state()
|
||||
private solution: string = '';
|
||||
|
||||
@state()
|
||||
private challengeData: Challenge | null = null;
|
||||
|
||||
@state()
|
||||
private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
|
||||
|
||||
@state()
|
||||
private disabled: boolean = true;
|
||||
|
||||
// stores the nonce and solution atomics
|
||||
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||
|
||||
private solverWorkers: Worker[] | null = null;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchChallenge();
|
||||
|
||||
this.initWorkers();
|
||||
|
||||
this.addEventListener('reset', (ev) => this.reset(ev as CustomEvent));
|
||||
this.addEventListener('solve', () => this.solveChallenge());
|
||||
|
||||
switch (this.auto) {
|
||||
case 'onload':
|
||||
this.solveChallenge();
|
||||
break;
|
||||
case 'onfocus':
|
||||
this.addEventListener('focus', () => this.solveChallenge());
|
||||
break;
|
||||
case 'onsubmit':
|
||||
if (this.parentElement?.nodeName === 'FORM') {
|
||||
this.parentElement.addEventListener('submit', () => this.solveChallenge());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
for (const worker of this.solverWorkers || []) {
|
||||
worker.terminate();
|
||||
this.solverWorkers = null;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
return Atomics.load(new Uint32Array(this.sab), 0);
|
||||
}
|
||||
|
||||
async fetchChallenge() {
|
||||
if (this.challengeData !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.challengejson !== '' && this.challengeData === null) {
|
||||
this.challengeData = JSON.parse(this.challengejson);
|
||||
this.challengejson = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`${this.challengeUrl}/challenge`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.challengeData = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching challenge:', error);
|
||||
console.error('Failed to fetch challenge');
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
this.solverWorkers = [];
|
||||
|
||||
const num_workers = navigator.hardwareConcurrency || 4;
|
||||
for (let i = 0; i < num_workers; i++) {
|
||||
this.solverWorkers.push(new ChallengeWorker());
|
||||
}
|
||||
|
||||
let wasm_module = await get_wasm_module();
|
||||
let worker_promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < this.solverWorkers.length; i++) {
|
||||
console.log('Worker', i);
|
||||
const worker = this.solverWorkers[i]!;
|
||||
|
||||
worker_promises.push(new Promise<void>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
console.log('Worker', i, 'got message', event.data);
|
||||
|
||||
if (event.data.type === WorkerResponseType.Error) {
|
||||
console.error("Worker error:", event.data.error);
|
||||
reject(event.data.error);
|
||||
}
|
||||
|
||||
if (event.data.type === WorkerResponseType.InitOk) {
|
||||
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: wasm_module,
|
||||
sab: this.sab,
|
||||
} as WorkerRequest);
|
||||
}));
|
||||
}
|
||||
|
||||
const timeoutMs = 10 * 1000;
|
||||
let timeout: number;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
console.error('Failed to initialize workers in time');
|
||||
this.status = 'error';
|
||||
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
Promise.allSettled(worker_promises).then(() => {
|
||||
console.log('All workers initialized');
|
||||
}),
|
||||
timeoutPromise,
|
||||
]).then(() => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async issueChallengeToWorker(worker: Worker, request: ChallengeSolveRequest): 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,
|
||||
// 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) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
worker.postMessage({
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt: request.salt,
|
||||
difficulty: request.difficulty,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
worker.postMessage({
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: request.target,
|
||||
salt: request.salt,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.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() {
|
||||
if (!this.challengeData || this.solverWorkers === null) {
|
||||
console.error('solveChallenge called before challenge is ready');
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.solution !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
|
||||
console.log(this.challengeData);
|
||||
|
||||
this.status = 'solving';
|
||||
this.solution = '';
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, -1);
|
||||
|
||||
let request: ChallengeSolveRequest;
|
||||
|
||||
switch (this.challengeData.algorithm) {
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
switch (this.challengeData.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
request = {
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt: this.challengeData.salt,
|
||||
difficulty: this.challengeData.difficulty,
|
||||
};
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
request = {
|
||||
algorithm: ChallengeAlgorithm.SHA256,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: this.challengeData.target,
|
||||
salt: this.challengeData.salt,
|
||||
};
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.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...');
|
||||
// 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
|
||||
// unblock, they return 0 since the challenge is already solved.
|
||||
//
|
||||
// TODO: We need to do a better job of tracking solvers, so if one worker
|
||||
// errors out, we only error out if all workers have errored out.
|
||||
let worker_promises: Promise<SolutionMessage>[] = [];
|
||||
if (request.algorithm === ChallengeAlgorithm.kCTF) {
|
||||
worker_promises.push(this.issueChallengeToWorker(this.solverWorkers[0], request));
|
||||
} else {
|
||||
for (let worker of this.solverWorkers) {
|
||||
// dispatch to all workers, func is async so it will not block
|
||||
worker_promises.push(this.issueChallengeToWorker(worker, request));
|
||||
}
|
||||
}
|
||||
|
||||
let solution = await Promise.race(worker_promises);
|
||||
|
||||
if (solution.type === WorkerResponseType.Error) {
|
||||
console.error("Worker error:", solution.error);
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (solution.type !== WorkerResponseType.Solution) {
|
||||
console.error("Worker sent spurious message");
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: configure if we should fetch or not
|
||||
try {
|
||||
await fetch(`${this.challengeUrl}/challenge`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
salt: this.challengeData.salt,
|
||||
solution: solution.solution,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
this.status = 'solved';
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
salt: this.challengeData.salt,
|
||||
solution: solution.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
} catch {
|
||||
console.error('Failed to submit solution');
|
||||
this.status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
solvePreventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.solveChallenge();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.challengejson !== '' && this.challengeData === null) {
|
||||
this.challengeData = JSON.parse(this.challengejson);
|
||||
this.challengejson = '';
|
||||
}
|
||||
|
||||
if (this.challengeData === null) {
|
||||
return html`
|
||||
<div class="loading-message">Loading captcha challenge...</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="impost-widget">
|
||||
<div class="impost-main">
|
||||
<div class="impost-checkbox">
|
||||
${this.status !== 'solving' ? html`${this.status === 'error' ? html`<svg class="impost-error-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 14q-.425 0-.712-.288T11 13V6q0-.425.288-.712T12 5t.713.288T13 6v7q0 .425-.288.713T12 14m0 5q-.425 0-.712-.288T11 18t.288-.712T12 17t.713.288T13 18t-.288.713T12 19"/></svg>` : html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||
`}` : html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||
<g stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="9.5" fill="none" stroke-linecap="round" stroke-width="3">
|
||||
<animate attributeName="stroke-dasharray" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0 150;42 150;42 150;42 150"/>
|
||||
<animate attributeName="stroke-dashoffset" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0;-16;-59;-59"/>
|
||||
</circle>
|
||||
<animateTransform attributeName="transform" dur="2s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
|
||||
</g>
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<label for="impost-checkbox-${this.uid}">${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
</div>
|
||||
|
||||
<div class="impost-footer">
|
||||
<div id="provider-link">
|
||||
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type = "text"
|
||||
id = "impost-solution"
|
||||
class="hidden"
|
||||
.value = ${this.solution}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,26 @@
|
||||
// This worker just sits on another thread and waits for message to solve
|
||||
// challenges so that we dont block the render thread
|
||||
|
||||
import { type Challenge } from "@impost/lib";
|
||||
import { type SolverModule, init_solver, solve_challenge as libimpost_solve_challenge } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
type WorkerRequest,
|
||||
type SolutionMessage,
|
||||
WorkerMessageType,
|
||||
WorkerResponseType,
|
||||
} from "./types/worker";
|
||||
|
||||
import { type SolverModule, init_solver, solve, type SolveParams } from '@impost/lib/solver';
|
||||
import { ChallengeStrategy, ChallengeAlgorithm } from '@impost/lib';
|
||||
|
||||
let solver: SolverModule | null = null;
|
||||
|
||||
let atomic_nonce: Int32Array | null = null;
|
||||
let atomic_solution: Int32Array | null = null;
|
||||
|
||||
/**
|
||||
* Initializes the worker.
|
||||
* Must be called before any solve functions.
|
||||
* @param module The solver web assembly module.
|
||||
* @param sab The SharedArrayBuffer for nonce and solution.
|
||||
*/
|
||||
export async function init(module: WebAssembly.Module, sab: SharedArrayBuffer): Promise<void> {
|
||||
atomic_nonce = new Int32Array(sab, 0, 1);
|
||||
atomic_solution = new Int32Array(sab, 4, 1);
|
||||
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
if (event.data.type === WorkerMessageType.Init) {
|
||||
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||
let module = event.data.module;
|
||||
|
||||
try {
|
||||
solver = await init_solver({
|
||||
@@ -29,36 +31,85 @@ export async function init(module: WebAssembly.Module, sab: SharedArrayBuffer):
|
||||
__log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(solver!.exports.memory.buffer, str_ptr, str_len))),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
console.error("Worker: Failed to initialize WASM solver", error);
|
||||
throw new Error(`Could not load WASM solver in worker: ${error.message}`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: `Could not load WASM solver: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
throw new Error("Worker: Failed to load WASM solver.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves a given challenge.
|
||||
* @param challenge The challenge data.
|
||||
* @returns The nonce solution on success, or throws an error.
|
||||
*/
|
||||
export async function solve_challenge(challenge: Challenge): Promise<string> {
|
||||
if (!solver || !atomic_nonce || !atomic_solution) {
|
||||
throw new Error("WASM solver or atomics not initialized in worker. Call init() first.");
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to load WASM solver",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Worker: Solving challenge...");
|
||||
|
||||
const solutionNonce = libimpost_solve_challenge(solver, challenge);
|
||||
|
||||
if (solutionNonce < 0) {
|
||||
throw new Error(`Worker: Failed to solve challenge. Internal code: ${solutionNonce}`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.InitOk,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const finalSolution = Atomics.load(atomic_solution, 0);
|
||||
return finalSolution.toString();
|
||||
}
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "WASM solver not loaded",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// * Do not forget to expose functions we want to use on the main thread
|
||||
Comlink.expose({ solve_challenge, init });
|
||||
let solution: string | number;
|
||||
try {
|
||||
let params = {
|
||||
name: event.data.algorithm,
|
||||
salt: event.data.salt,
|
||||
};
|
||||
|
||||
switch (event.data.algorithm) {
|
||||
case ChallengeAlgorithm.Argon2:
|
||||
case ChallengeAlgorithm.SHA256:
|
||||
switch (event.data.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
// @ts-ignore
|
||||
params.strategy = ChallengeStrategy.LeadingZeroes;
|
||||
// @ts-ignore
|
||||
params.difficulty = event.data.difficulty;
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
// @ts-ignore
|
||||
params.strategy = ChallengeStrategy.TargetNumber;
|
||||
// @ts-ignore
|
||||
params.target = event.data.target;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ChallengeAlgorithm.kCTF:
|
||||
// @ts-ignore
|
||||
params.strategy = ChallengeStrategy.Null;
|
||||
// @ts-ignore
|
||||
params.difficulty = event.data.difficulty;
|
||||
break;
|
||||
}
|
||||
|
||||
solution = solve(solver, params as SolveParams);
|
||||
|
||||
if (event.data.algorithm !== ChallengeAlgorithm.kCTF) {
|
||||
console.log(Atomics.load(atomic_nonce!, 0));
|
||||
solution = Atomics.load(atomic_solution!, 0);
|
||||
}
|
||||
} catch (error: any) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: `Failed to solve challenge: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Solution,
|
||||
solution,
|
||||
} as SolutionMessage);
|
||||
};
|
||||
|
||||
72
packages/widget/src/types/worker.ts
Normal file
72
packages/widget/src/types/worker.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ChallengeAlgorithm, ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
export enum WorkerMessageType {
|
||||
Init = "init",
|
||||
Challenge = "challenge",
|
||||
}
|
||||
|
||||
interface WorkerInitRequest {
|
||||
type: WorkerMessageType.Init;
|
||||
module: WebAssembly.Module;
|
||||
|
||||
sab: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
interface ChallengeLeadingZeroesSolveRequest {
|
||||
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
interface ChallengeTargetNumberSolveRequest {
|
||||
algorithm: ChallengeAlgorithm.SHA256 | ChallengeAlgorithm.Argon2;
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
target: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
interface ChallengekCTFSolveRequest {
|
||||
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 enum WorkerResponseType {
|
||||
Error = "error",
|
||||
InitOk = "init_ok",
|
||||
Solution = "solution",
|
||||
}
|
||||
|
||||
interface ErrorMessageResponse {
|
||||
type: WorkerResponseType.Error;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface SolutionMessageResponse {
|
||||
type: WorkerResponseType.Solution;
|
||||
solution: string | number;
|
||||
}
|
||||
|
||||
interface InitOkMessageResponse {
|
||||
type: WorkerResponseType.InitOk;
|
||||
}
|
||||
|
||||
export type SolutionMessage = ErrorMessageResponse | SolutionMessageResponse | InitOkMessageResponse;
|
||||
@@ -17,10 +17,13 @@ export default defineConfig({
|
||||
dir: 'dist',
|
||||
},
|
||||
},
|
||||
minify: true
|
||||
// WARN: this setting has caused issues for me in the past, but now it
|
||||
// seems to work fine. I'm not sure why, but minifying the bundle saves
|
||||
// 5KB, and it doesnt *cause* issues, so I'm leaving it on.
|
||||
minify: true,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
dts()
|
||||
dts(),
|
||||
],
|
||||
});
|
||||
8648
pnpm-lock.yaml
generated
8648
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
packages:
|
||||
- packages/*
|
||||
- example-app/
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- core-js-pure
|
||||
- esbuild
|
||||
|
||||
shamefully-hoist: true
|
||||
15
solver/src/algorithms/algorithms.zig
Normal file
15
solver/src/algorithms/algorithms.zig
Normal 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");
|
||||
17
solver/src/algorithms/argon2.zig
Normal file
17
solver/src/algorithms/argon2.zig
Normal 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;
|
||||
}
|
||||
155
solver/src/algorithms/kctf.zig
Normal file
155
solver/src/algorithms/kctf.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
9
solver/src/algorithms/sha256.zig
Normal file
9
solver/src/algorithms/sha256.zig
Normal 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;
|
||||
}
|
||||
291
solver/src/solver.zig
Normal file
291
solver/src/solver.zig
Normal file
@@ -0,0 +1,291 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const algorithms = @import("algorithms/algorithms.zig");
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
extern fn __get_solution() i32;
|
||||
extern fn __set_solution(value: i32) void;
|
||||
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
|
||||
extern fn __fetch_add_nonce(value: i32) i32;
|
||||
extern fn __log(str_ptr: usize, str_len: usize) void;
|
||||
|
||||
fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
|
||||
if (comptime builtin.target.cpu.arch != .wasm32) {
|
||||
std.log.defaultLog(level, scope, fmt, args);
|
||||
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 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
return @ptrCast(ptr.ptr);
|
||||
}
|
||||
|
||||
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
if (ptr) |p| {
|
||||
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||
allocator.free(cast_ptr[0..byte_count]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Both SHA256 and Argon2 are thread safe and are explicitly designed to be used in a multithreaded environment.
|
||||
/// kCTF is designed only to be used in a single threaded environment. It does not use the same nonce atomics,
|
||||
/// and duplicates work if solved across multiple threads.
|
||||
///
|
||||
/// If a target is not needed for the strategy, target_ptr and target_len should be 0.
|
||||
export fn solve(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, salt_ptr: [*]u8, salt_len: usize, difficulty: usize, target_ptr: [*]u8, target_len: usize) isize {
|
||||
switch (algorithm) {
|
||||
algorithms.Algorithm.sha256 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len),
|
||||
algorithms.Algorithm.argon2 => return solve_argon2_or_sha256(salt_ptr, salt_len, difficulty, algorithm, strategy, target_ptr, target_len),
|
||||
algorithms.Algorithm.kctf => {
|
||||
if (strategy != algorithms.Strategy.null) {
|
||||
std.log.err("kCTF does not support a strategy", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
return solve_kctf(salt_ptr, salt_len, difficulty);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn solve_argon2_or_sha256(salt_ptr: [*]u8, salt_len: usize, difficulty: usize, algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, target_ptr: [*]u8, target_len: usize) isize {
|
||||
if (strategy == algorithms.Strategy.null) {
|
||||
std.log.err("Argon2 needs a strategy", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strategy == .leading_zeros) {
|
||||
if (difficulty < 1 or difficulty > 64) {
|
||||
std.log.err("Argon2 difficulty must be between 1 and 64 when using leading_zeros", .{});
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
const salt_slice = salt_ptr[0..salt_len];
|
||||
var target_slice: ?[]u8 = null;
|
||||
if (@intFromPtr(target_ptr) != 0) {
|
||||
target_slice = target_ptr[0..target_len];
|
||||
}
|
||||
|
||||
if (strategy == .target_number and target_slice == null) {
|
||||
std.log.err("A target must be specified when using the target_number strategy", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
const max_nonce_iterations: u64 = 1_000_000_000;
|
||||
// const max_nonce_iterations: u64 = 100_000;
|
||||
|
||||
// 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations)
|
||||
var input_buffer: []u8 = allocator.alloc(u8, salt_len + 9) catch {
|
||||
std.log.err("Out of memory", .{});
|
||||
return -1;
|
||||
};
|
||||
// dont leak memory :pepega:
|
||||
defer allocator.free(input_buffer);
|
||||
|
||||
@memcpy(input_buffer[0..salt_len], salt_slice);
|
||||
|
||||
var nonce = __fetch_add_nonce(1);
|
||||
var hex_encoder = utils.HexEncoder{};
|
||||
var input: []u8 = undefined;
|
||||
|
||||
while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
|
||||
if (__get_solution() != -1) {
|
||||
// solution has already been found, no point in continuing
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nonce_str = std.fmt.bufPrint(input_buffer[salt_len..], "{d}", .{nonce}) catch {
|
||||
std.log.err("Error formatting nonce", .{});
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (algorithm == .argon2) {
|
||||
input = algorithms.Argon2.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch {
|
||||
std.log.err("Error hashing salt", .{});
|
||||
return -1;
|
||||
};
|
||||
} else {
|
||||
input = algorithms.SHA256.hash(allocator, input_buffer[0 .. salt_len + nonce_str.len]) catch {
|
||||
std.log.err("Error hashing salt", .{});
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
.leading_zeros => {
|
||||
_ = hex_encoder.encode(input);
|
||||
allocator.free(input);
|
||||
if (hex_encoder.countZeroes(difficulty)) {
|
||||
// Found a solution!
|
||||
if (__cmpxchg_solution(-1, nonce) == -1) {
|
||||
// we found a solution, and we are the first to do so
|
||||
return nonce;
|
||||
} else {
|
||||
// we found a solution, but we are not the first to do so
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
.target_number => {
|
||||
const hex = hex_encoder.encode(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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// value_ptr is a just the base64 challenge string (e.g. "xxxxxxxxx==")
|
||||
fn solve_kctf(value_ptr: [*]u8, value_len: usize, difficulty: usize) isize {
|
||||
if (difficulty < 1) {
|
||||
std.log.err("KCTF difficulty must be at least 1", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
const challenge_slice = value_ptr[0..value_len];
|
||||
|
||||
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});
|
||||
}
|
||||
@@ -10,12 +10,7 @@ pub const HexEncoder = struct {
|
||||
pub fn encode(self: *Self, bytes: []const u8) []u8 {
|
||||
self.scratch_set = true;
|
||||
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
self.scratch[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||
self.scratch[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||
}
|
||||
bytesToHex(bytes, &self.scratch);
|
||||
|
||||
return &self.scratch;
|
||||
}
|
||||
@@ -41,3 +36,12 @@ pub const HexEncoder = struct {
|
||||
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];
|
||||
}
|
||||
}
|
||||
177
solver/src/validator.zig
Normal file
177
solver/src/validator.zig
Normal file
@@ -0,0 +1,177 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const algorithms = @import("algorithms/algorithms.zig");
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
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 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
return @ptrCast(ptr.ptr);
|
||||
}
|
||||
|
||||
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
if (ptr) |p| {
|
||||
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||
allocator.free(cast_ptr[0..byte_count]);
|
||||
}
|
||||
}
|
||||
|
||||
export fn validate(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, nonce: usize, difficulty: usize) bool {
|
||||
switch (algorithm) {
|
||||
algorithms.Algorithm.sha256 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.sha256, strategy),
|
||||
algorithms.Algorithm.argon2 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.argon2, strategy),
|
||||
algorithms.Algorithm.kctf => return validate_kctf(challenge_ptr, challenge_len, solution_ptr, solution_len, difficulty),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_argon2_or_sha256(challenge_ptr: [*]u8, challenge_len: usize, nonce: usize, target_ptr: [*]u8, target_len: usize, difficulty: usize, algorithm: algorithms.Algorithm, strategy: algorithms.Strategy) bool {
|
||||
if (strategy == algorithms.Strategy.null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strategy == .leading_zeros) {
|
||||
if (difficulty < 1 or difficulty > 64) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||
const nonce_slice = std.fmt.allocPrint(allocator, "{d}", .{nonce}) catch return false;
|
||||
|
||||
var target_slice: ?[]u8 = null;
|
||||
if (@intFromPtr(target_ptr) != 0) {
|
||||
target_slice = target_ptr[0..target_len];
|
||||
}
|
||||
|
||||
if (strategy == .target_number and target_slice == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const input_slice = allocator.alloc(u8, challenge_len + nonce_slice.len) catch return false;
|
||||
defer allocator.free(input_slice);
|
||||
@memcpy(input_slice[0..challenge_len], challenge_slice);
|
||||
@memcpy(input_slice[challenge_len..], nonce_slice);
|
||||
|
||||
var input: []u8 = undefined;
|
||||
if (algorithm == .argon2) {
|
||||
input = algorithms.Argon2.hash(allocator, input_slice[0..challenge_len], input_slice[challenge_len .. challenge_len + nonce_slice.len]) catch return false;
|
||||
} 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;
|
||||
}
|
||||
|
||||
fn validate_kctf(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, difficulty: usize) bool {
|
||||
const challenge_buf = challenge_ptr[0..challenge_len];
|
||||
const solution_buf = solution_ptr[0..solution_len];
|
||||
|
||||
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;
|
||||
defer {
|
||||
challenge.destroy(allocator);
|
||||
solution.destroy(allocator);
|
||||
}
|
||||
|
||||
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, algorithm: algorithms.Algorithm) u64 {
|
||||
const challenge = challenge_ptr[0..challenge_len];
|
||||
const nonce = nonce_ptr[0..nonce_len];
|
||||
|
||||
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
|
||||
var ret: u64 = heap_hex_slice.len;
|
||||
ret <<= 32;
|
||||
ret |= @intFromPtr(heap_hex_slice.ptr);
|
||||
allocator.free(hash_slice);
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
// TODO
|
||||
// if (comptime builtin.cpu.arch == .wasm32) return;
|
||||
|
||||
// const args = try std.process.argsAlloc(allocator);
|
||||
// if (args.len < 3) {
|
||||
// std.log.err("Usage: zig run src/validator.zig <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});
|
||||
}
|
||||
Reference in New Issue
Block a user