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