Initial commit
Once again a weird place to commit, I have already done a lot of work, but I am just bad at using git, okay.
This commit is contained in:
2
packages/lib/.gitignore
vendored
Normal file
2
packages/lib/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
60
packages/lib/README.md
Normal file
60
packages/lib/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# @impost/lib
|
||||
|
||||
This package contains the types and WASM code for the impost. It is useful for
|
||||
building other packages that want to use the WASM code, a client, or a server.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
validate_challenge,
|
||||
generate_target_number_challenge,
|
||||
} from "@impost/lib/validator";
|
||||
import {
|
||||
init_solver,
|
||||
get_wasm_module,
|
||||
solve_target_number_challenge,
|
||||
} from "@impost/lib/solver";
|
||||
import { type Challenge, ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
const challenge = await generate_target_number_challenge(
|
||||
// timeout in ms
|
||||
{
|
||||
// impost automatically recognizes this parameter, its also recommended to
|
||||
// add a timestamp to prevent replay or pre-computation attacks
|
||||
expires_at: Date.now() + 3600,
|
||||
}
|
||||
// max number
|
||||
1_000
|
||||
);
|
||||
|
||||
if (challenge === null) {
|
||||
throw new Error("Failed to generate challenge");
|
||||
}
|
||||
|
||||
let solver = await init_solver(
|
||||
{
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) =>
|
||||
Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) =>
|
||||
Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
},
|
||||
await get_wasm_module()
|
||||
);
|
||||
|
||||
const solution = await solve_target_number_challenge(solver, {
|
||||
salt: challenge.salt,
|
||||
target: challenge.target,
|
||||
});
|
||||
|
||||
const is_valid = await validate_challenge(challenge, {
|
||||
challenge: challenge.target,
|
||||
nonce: solution,
|
||||
});
|
||||
|
||||
if (is_valid) {
|
||||
console.log("Challenge solved!", solution);
|
||||
}
|
||||
```
|
||||
2498
packages/lib/package-lock.json
generated
Normal file
2498
packages/lib/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
packages/lib/package.json
Normal file
39
packages/lib/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@impost/lib",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"license": "BSL-1.0",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./solver": {
|
||||
"types": "./dist/solver.d.ts",
|
||||
"import": "./dist/solver.js"
|
||||
},
|
||||
"./validator": {
|
||||
"types": "./dist/validator.d.ts",
|
||||
"import": "./dist/validator.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"watch": "tsc && vite build --watch",
|
||||
"dev": "vite",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"oxc-minify": "^0.97.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuidv7": "^1.0.2"
|
||||
}
|
||||
}
|
||||
28
packages/lib/src/index.ts
Normal file
28
packages/lib/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum ChallengeAlgorithm {
|
||||
Argon2id = "argon2id",
|
||||
}
|
||||
|
||||
export enum ChallengeStrategy {
|
||||
LeadingZeroes = "leading_zeroes",
|
||||
TargetNumber = "target_number",
|
||||
}
|
||||
|
||||
// In this case, the client will repeatedly hash a number with has until it
|
||||
// finds a hash thaat starts with *difficulty* leading zeroes
|
||||
export interface ChallengeLeadingZeroes {
|
||||
algorithm: ChallengeAlgorithm;
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string; // random string
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
// In this case, the server generates a random number, and the client will hash
|
||||
// the salt (a random string) + a random number until it finds a hash that is equal to challenge
|
||||
export interface ChallengeTargetNumber {
|
||||
algorithm: ChallengeAlgorithm;
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
salt: string; // random string
|
||||
target: string; // hash of salt + random number
|
||||
}
|
||||
|
||||
export type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber;
|
||||
92
packages/lib/src/solver.ts
Normal file
92
packages/lib/src/solver.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
"free": (ptr: number | null, byte_count: number) => void;
|
||||
"solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number;
|
||||
"solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: number) => number;
|
||||
"memory": WebAssembly.Memory;
|
||||
}
|
||||
|
||||
export interface SolverModule extends WebAssembly.Instance {
|
||||
exports: WasmExports;
|
||||
}
|
||||
|
||||
export type SolverEnv = {
|
||||
__get_solution: () => number;
|
||||
__set_solution: (value: number) => void;
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => number;
|
||||
__fetch_add_nonce: (value: number) => number;
|
||||
};
|
||||
|
||||
export async function get_wasm_module(): Promise<WebAssembly.Module> {
|
||||
return WebAssembly.compileStreaming(fetch(WASMSolverUrl));;
|
||||
}
|
||||
|
||||
export async function init_solver(env: SolverEnv, module: WebAssembly.Module): Promise<SolverModule> {
|
||||
return await WebAssembly.instantiate(module, {
|
||||
env,
|
||||
}) as unknown as SolverModule;
|
||||
}
|
||||
|
||||
export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number {
|
||||
const { salt, difficulty } = challenge;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const salt_bytes = encoder.encode(salt);
|
||||
|
||||
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||
if (salt_ptr === 0 || salt_ptr === null) {
|
||||
throw new Error("Failed to allocate memory for challenge string");
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
|
||||
const ret = solver.exports.solve_leaading_zeroes_challenge(
|
||||
salt_ptr,
|
||||
salt_bytes.length,
|
||||
difficulty,
|
||||
);
|
||||
|
||||
if (ret < 0) {
|
||||
throw new Error("Failed to solve challenge");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number {
|
||||
const { salt, target } = challenge;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const salt_bytes = encoder.encode(salt);
|
||||
const target_bytes = encoder.encode(target);
|
||||
|
||||
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||
if (salt_ptr === 0 || salt_ptr === null) {
|
||||
throw new Error("Failed to allocate memory for salt string");
|
||||
}
|
||||
|
||||
const target_ptr = solver.exports.malloc(target_bytes.length);
|
||||
if (target_ptr === 0 || target_ptr === null) {
|
||||
throw new Error("Failed to allocate memory for target string");
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
|
||||
const ret = solver.exports.solve_target_number_challenge(
|
||||
target_ptr,
|
||||
target_bytes.length,
|
||||
salt_ptr,
|
||||
salt_bytes.length,
|
||||
);
|
||||
|
||||
if (ret < 0) {
|
||||
throw new Error("Failed to solve challenge");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
172
packages/lib/src/validator.ts
Normal file
172
packages/lib/src/validator.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.';
|
||||
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
"free": (ptr: number | null, byte_count: number) => void;
|
||||
"validate_leading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number, difficulty: number) => number;
|
||||
"validate_target_number_challenge": (target_ptr: number, target_len: number, nonce_ptr: number, nonce_len: number, salt_ptr: number, salt_len: number) => number;
|
||||
"hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint;
|
||||
"memory": WebAssembly.Memory;
|
||||
}
|
||||
|
||||
export interface ValidatorModule extends WebAssembly.Instance {
|
||||
exports: WasmExports;
|
||||
}
|
||||
|
||||
function array_to_base64(buffer: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
}
|
||||
|
||||
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<Challenge> {
|
||||
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
|
||||
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
|
||||
|
||||
let challenge: Challenge = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt,
|
||||
difficulty,
|
||||
};
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
async function generate_target_number_challenge(parameters: Object, max_number: number): Promise<Challenge | null> {
|
||||
// in target number config, since we need to generate a target hash, we
|
||||
// need to hash the salt + nonce, so the client knows what the target is
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule;
|
||||
|
||||
let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
|
||||
let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
|
||||
let random_number = new DataView(crypto.getRandomValues(new Uint8Array(4)).buffer).getUint32(0, true) % max_number;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const salt_bytes = encoder.encode(salt);
|
||||
const random_number_bytes = encoder.encode(random_number.toString());
|
||||
|
||||
const salt_ptr = validator.exports.malloc(salt_bytes.length);
|
||||
const random_number_ptr = validator.exports.malloc(random_number_bytes.length);
|
||||
|
||||
if (salt_ptr === 0 || salt_ptr === null || random_number_ptr === 0 || random_number_ptr === null) {
|
||||
console.error("Failed to allocate memory for challenge string");
|
||||
return null;
|
||||
}
|
||||
|
||||
const memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(random_number_bytes, random_number_ptr);
|
||||
|
||||
let target_blob: bigint = validator.exports.hash(salt_ptr, salt_bytes.length, random_number_ptr, random_number_bytes.length);
|
||||
let target_ptr = Number(target_blob & BigInt(0xFFFFFFFF));
|
||||
let target_len = Number(target_blob >> BigInt(32));
|
||||
|
||||
validator.exports.free(salt_ptr, salt_bytes.length);
|
||||
validator.exports.free(random_number_ptr, random_number_bytes.length);
|
||||
|
||||
// do NOT use `memory` here, by this time it has almost definitely been resized and will cause errors to touch
|
||||
let target_slice = new Uint8Array(validator.exports.memory.buffer.slice(target_ptr, target_ptr + target_len));
|
||||
const target = new TextDecoder().decode(target_slice);
|
||||
|
||||
let challenge: Challenge = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
salt,
|
||||
target
|
||||
};
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
export interface LeadingZeroesChallengeConfig {
|
||||
parameters: Object;
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
export interface TargetNumberChallengeConfig {
|
||||
parameters: Object;
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
max_number: number;
|
||||
}
|
||||
|
||||
export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig;
|
||||
|
||||
export async function generate_challenge(config: ChallengeConfig): Promise<Challenge | null> {
|
||||
let challenge: Challenge | null = null;
|
||||
switch (config.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
challenge = await generate_leading_zeroes_challenge(config.parameters, config.difficulty);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
challenge = await generate_target_number_challenge(config.parameters, config.max_number);
|
||||
break;
|
||||
}
|
||||
|
||||
if (challenge === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise<boolean> {
|
||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
let err;
|
||||
let memory;
|
||||
let nonce_bytes, nonce_ptr;
|
||||
let target_bytes, target_ptr;
|
||||
switch (challenge.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
target_bytes = encoder.encode(challenge_solution.challenge);
|
||||
nonce_bytes = encoder.encode(challenge_solution.nonce);
|
||||
|
||||
target_ptr = validator.exports.malloc(challenge_solution.challenge.length);
|
||||
nonce_ptr = validator.exports.malloc(challenge_solution.nonce.length);
|
||||
|
||||
if (target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
|
||||
console.error("Failed to allocate memory for challenge string");
|
||||
return false;
|
||||
}
|
||||
|
||||
memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
memory.set(nonce_bytes, nonce_ptr);
|
||||
|
||||
err = validator.exports.validate_leading_zeroes_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, challenge.difficulty);
|
||||
|
||||
validator.exports.free(target_ptr, target_bytes.length);
|
||||
validator.exports.free(nonce_ptr, nonce_bytes.length);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
target_bytes = encoder.encode(challenge.target);
|
||||
const salt_bytes = encoder.encode(challenge.salt);
|
||||
nonce_bytes = encoder.encode(challenge_solution.nonce);
|
||||
|
||||
const salt_ptr = validator.exports.malloc(salt_bytes.length);
|
||||
target_ptr = validator.exports.malloc(target_bytes.length);
|
||||
nonce_ptr = validator.exports.malloc(nonce_bytes.length);
|
||||
|
||||
if (salt_ptr === 0 || salt_ptr === null || target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
|
||||
console.error("Failed to allocate memory for challenge string");
|
||||
return false;
|
||||
}
|
||||
|
||||
memory = new Uint8Array(validator.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
memory.set(nonce_bytes, nonce_ptr);
|
||||
|
||||
err = validator.exports.validate_target_number_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, salt_ptr, salt_bytes.length);
|
||||
|
||||
validator.exports.free(salt_ptr, salt_bytes.length);
|
||||
validator.exports.free(target_ptr, target_bytes.length);
|
||||
validator.exports.free(nonce_ptr, nonce_bytes.length);
|
||||
break;
|
||||
}
|
||||
|
||||
return err === 0;
|
||||
}
|
||||
1
packages/lib/src/vite-env.d.ts
vendored
Normal file
1
packages/lib/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
27
packages/lib/tsconfig.json
Normal file
27
packages/lib/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
29
packages/lib/vite.config.ts
Normal file
29
packages/lib/vite.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'es2022',
|
||||
rollupOptions: {
|
||||
input: [
|
||||
resolve(__dirname, 'src/index.ts'),
|
||||
resolve(__dirname, 'src/solver.ts'),
|
||||
resolve(__dirname, 'src/validator.ts'),
|
||||
],
|
||||
preserveEntrySignatures: "strict",
|
||||
output: {
|
||||
dir: 'dist',
|
||||
entryFileNames: '[name].js',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
sourcemap: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src'
|
||||
}
|
||||
},
|
||||
plugins: [dts()]
|
||||
});
|
||||
Reference in New Issue
Block a user