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:
Zoe
2025-11-25 18:09:17 +00:00
parent 570531fe32
commit e16383e9b9
20 changed files with 1262 additions and 476 deletions

View File

@@ -0,0 +1,15 @@
pub const Algorithm = enum(u8) {
sha256 = 0,
argon2 = 1,
kctf = 2,
};
pub const Strategy = enum(u8) {
null = 0,
leading_zeros = 1,
target_number = 2,
};
pub const SHA256 = @import("sha256.zig");
pub const Argon2 = @import("argon2.zig");
pub const kCTF = @import("kctf.zig");

View File

@@ -0,0 +1,17 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
var argon2_params = std.crypto.pwhash.argon2.Params{
.t = 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;
}

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

View File

@@ -0,0 +1,9 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn hash(allocator: Allocator, data: []const u8) ![]u8 {
const output_hash = try allocator.alloc(u8, std.crypto.hash.sha2.Sha256.digest_length);
std.crypto.hash.sha2.Sha256.hash(data, @ptrCast(output_hash), .{});
return output_hash;
}

View File

@@ -1,31 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
var argon2_params = std.crypto.pwhash.argon2.Params{
.t = 4, // time cost
.m = 256, // memory cost (in KiB)
.p = 1, // parallelism (this doesnt do anything because we are targeting wasm, and we do multithreading differently anyways)
};
const dk_len: usize = 32; // 16 or 32 byte key
var derived: [dk_len]u8 = undefined;
var buffer_hash_hex: [64]u8 = undefined;
fn bytesToHex(bytes: []const u8, output: []u8) void {
const hex_chars = "0123456789abcdef";
var i: usize = 0;
while (i < bytes.len) : (i += 1) {
output[i * 2] = hex_chars[(bytes[i] >> 4)];
output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
}
}
pub fn hash(allocator: Allocator, challenge: []const u8, nonce: []const u8) ![]u8 {
try std.crypto.pwhash.argon2.kdf(allocator, &derived, nonce, challenge, argon2_params, .argon2id);
var hash_bytes: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(&derived, @ptrCast(hash_bytes[0..].ptr), .{});
bytesToHex(&hash_bytes, &buffer_hash_hex);
return buffer_hash_hex[0..];
}

View File

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

View File

@@ -2,11 +2,16 @@ const std = @import("std");
const builtin = @import("builtin");
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 allocator = gpa.allocator();
extern fn __get_solution() i32;
extern fn __set_solution(value: i32) void;
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
extern fn __fetch_add_nonce(value: i32) i32;
extern fn __log(str_ptr: usize, str_len: usize) void;
fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
@@ -15,9 +20,18 @@ fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), com
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;
__log(@intFromPtr(formatted.ptr), formatted.len);
const log_str = std.fmt.allocPrint(allocator, "{s}{s}", .{ log_level_str, formatted }) catch return;
allocator.free(formatted);
__log(@intFromPtr(log_str.ptr), log_str.len);
allocator.free(log_str);
}
pub const std_options: std.Options = .{ .logFn = log };
@@ -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")
export fn solve(value_ptr: [*]u8, value_len: usize) usize {
/// Both SHA256 and Argon2 are thread safe and are explicitly designed to be used in a multithreaded environment.
/// kCTF is designed only to be used in a single threaded environment. It does not use the same nonce atomics,
/// and duplicates work if solved across multiple threads.
///
/// If a target is not needed for the strategy, target_ptr and target_len should be 0.
export fn solve(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, salt_ptr: [*]u8, salt_len: usize, difficulty: usize, target_ptr: [*]u8, target_len: usize) isize {
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];
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)});
return 0;
return -1;
};
defer challenge.destroy(allocator);
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)});
return 0;
return -1;
};
std.log.info("Solution: {s}\n", .{solution});
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)) {
return 0;
return -1;
}
const output_len: u16 = @intCast(output_slice.len - 2);
// convert to little endian
output_slice[0] = @intCast(output_len & 0xFF); // LSB
output_slice[1] = @intCast(output_len >> 8); // MSB
@memcpy(output_slice[2..4], "s.");
@memcpy(output_slice[4 .. 4 + solution.len], solution);
@memcpy(output_slice[2 .. 2 + solution.len], solution);
allocator.free(solution);
return @intFromPtr(output_ptr.ptr);
return @intCast(@intFromPtr(output_ptr.ptr));
}
pub fn main() anyerror!void {
@@ -77,12 +229,79 @@ pub fn main() anyerror!void {
var args = try std.process.argsAlloc(allocator);
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;
}
const challenge = try kCTF.decode(allocator, args[1]);
const solution = try kCTF.solve(allocator, challenge);
var algorithm: ?algorithms.Algorithm = null;
var strategy: algorithms.Strategy = algorithms.Strategy.null;
var target: ?[]u8 = null;
if (std.mem.eql(u8, args[1], "sha256")) {
algorithm = algorithms.Algorithm.sha256;
} else if (std.mem.eql(u8, args[1], "argon2")) {
algorithm = algorithms.Algorithm.argon2;
} else if (std.mem.eql(u8, args[1], "kctf")) {
algorithm = algorithms.Algorithm.kctf;
}
var i: usize = 2;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "--strategy")) {
if (args.len <= i + 1) {
std.log.err("Expected strategy after --strategy", .{});
return;
}
if (std.mem.eql(u8, args[i + 1], "leading_zeros")) {
strategy = algorithms.Strategy.leading_zeros;
}
if (std.mem.eql(u8, args[i + 1], "target_number")) {
strategy = algorithms.Strategy.target_number;
}
if (strategy == .null) {
std.log.err("Invalid strategy: {s}", .{args[i + 1]});
return;
}
i += 1;
}
if (std.mem.eql(u8, arg, "--target")) {
if (args.len <= i + 1) {
std.log.err("Expected target after --target", .{});
return;
}
target = args[i + 1];
i += 1;
}
if (std.mem.eql(u8, arg, "--help")) {
std.log.info("Options:\n", .{});
std.log.info(" --strategy <strategy>: Specify the strategy to use. This only applies to some algorithms.\n", .{});
std.log.info(" --target <target>: Specify the target hash when using the target_number strategy.\n", .{});
std.log.info(" --help: Print this help message\n", .{});
std.log.info("Usage: {s} <strategy> [options] <challenge>", .{args[0]});
return;
}
}
if (strategy == .null and algorithm != .kctf) {
std.log.warn("No strategy specified, defaulting to leading_zeros", .{});
strategy = algorithms.Strategy.leading_zeros;
}
if (strategy == .target_number and target == null) {
std.log.err("A target must be specified when using the target_number strategy", .{});
return;
}
const challenge = try algorithms.kCTF.decode(allocator, args[1]);
const solution = try algorithms.kCTF.solve(allocator, challenge);
std.log.info("Solution: {s}", .{solution});
}

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

@@ -0,0 +1,47 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const HexEncoder = struct {
scratch: [64]u8 = undefined,
scratch_set: bool = false,
const Self = @This();
pub fn encode(self: *Self, bytes: []const u8) []u8 {
self.scratch_set = true;
bytesToHex(bytes, &self.scratch);
return &self.scratch;
}
// counts the number of leading hexidecimal zeroes in the scratch buffer
// which is set by encode
pub fn countZeroes(self: *Self, zeroes: usize) bool {
if (!self.scratch_set) {
return false;
}
if (zeroes > 64 or zeroes == 0) {
return false;
}
var i: usize = 0;
while (i < zeroes) : (i += 1) {
if (self.scratch[i] != '0') {
return false;
}
}
return true;
}
};
fn bytesToHex(bytes: []const u8, output: []u8) void {
const hex_chars = "0123456789abcdef";
var i: usize = 0;
while (i < bytes.len) : (i += 1) {
output[i * 2] = hex_chars[(bytes[i] >> 4)];
output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
}
}

View File

@@ -2,7 +2,8 @@ const std = @import("std");
const builtin = @import("builtin");
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 allocator = gpa.allocator();
@@ -34,26 +35,81 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
}
}
fn bytesToHex(bytes: []const u8, buf: []u8) void {
const hex_chars = "0123456789abcdef";
var i: usize = 0;
while (i < bytes.len) : (i += 1) {
buf[i * 2] = hex_chars[(bytes[i] >> 4)];
buf[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
export fn validate(algorithm: algorithms.Algorithm, strategy: algorithms.Strategy, challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize, nonce: usize, difficulty: usize) bool {
switch (algorithm) {
algorithms.Algorithm.sha256 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.sha256, strategy),
algorithms.Algorithm.argon2 => return validate_argon2_or_sha256(challenge_ptr, challenge_len, nonce, solution_ptr, solution_len, difficulty, algorithms.Algorithm.argon2, strategy),
algorithms.Algorithm.kctf => return validate_kctf(challenge_ptr, challenge_len, solution_ptr, solution_len, difficulty),
}
}
// challenge_ptr should look like s.<difficulty>.<challenge>
// solution_ptr should look like s.<solved_hash>
export fn validate(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize) bool {
fn validate_argon2_or_sha256(challenge_ptr: [*]u8, challenge_len: usize, nonce: usize, target_ptr: [*]u8, target_len: usize, difficulty: usize, algorithm: algorithms.Algorithm, strategy: algorithms.Strategy) bool {
if (strategy == algorithms.Strategy.null) {
return false;
}
if (strategy == .leading_zeros) {
if (difficulty < 1 or difficulty > 64) {
return false;
}
}
const challenge_slice = challenge_ptr[0..challenge_len];
const nonce_slice = std.fmt.allocPrint(allocator, "{d}", .{nonce}) catch return false;
var target_slice: ?[]u8 = null;
if (@intFromPtr(target_ptr) != 0) {
target_slice = target_ptr[0..target_len];
}
if (strategy == .target_number and target_slice == null) {
return false;
}
const input_slice = allocator.alloc(u8, challenge_len + nonce_slice.len) catch return false;
defer allocator.free(input_slice);
@memcpy(input_slice[0..challenge_len], challenge_slice);
@memcpy(input_slice[challenge_len..], nonce_slice);
var input: []u8 = undefined;
if (algorithm == .argon2) {
input = algorithms.Argon2.hash(allocator, input_slice[0..challenge_len], input_slice[challenge_len .. challenge_len + nonce_slice.len]) catch return false;
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 solution_buf = solution_ptr[0..solution_len];
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});
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 {
challenge.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", .{});
const is_valid = kCTF.check(allocator, challenge, solution) catch return false;
const is_valid = challenge.verify(allocator, solution) catch return false;
return is_valid;
}
pub fn main() anyerror!void {
if (comptime builtin.cpu.arch == .wasm32) return;
export fn hash(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, algorithm: algorithms.Algorithm) u64 {
const challenge = challenge_ptr[0..challenge_len];
const nonce = nonce_ptr[0..nonce_len];
const args = try std.process.argsAlloc(allocator);
if (args.len < 3) {
std.log.err("Usage: zig run src/validator.zig <challenge> <solution>", .{});
return;
var hash_slice: []u8 = undefined;
switch (algorithm) {
algorithms.Algorithm.sha256 => {
const input_slice = allocator.alloc(u8, challenge_len + nonce_len) catch return 0;
defer allocator.free(input_slice);
@memcpy(input_slice[0..challenge_len], challenge);
@memcpy(input_slice[challenge_len..], nonce);
hash_slice = algorithms.SHA256.hash(allocator, input_slice[0 .. challenge_len + nonce_len]) catch return 0;
},
algorithms.Algorithm.argon2 => {
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]);
defer challenge.destroy(allocator);
var hex_encoder = utils.HexEncoder{};
const hex_slice = hex_encoder.encode(hash_slice);
// hex_slice is stack allocated, therefore, if we pass it to the caller without copying it onto the heap, we are
// potentially (and likely) sending garbage memory to the caller
const heap_hex_slice = allocator.dupe(u8, hex_slice) catch return 0;
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});
// bs to get the compiler to not whine about hash_slice.len being a u5 annd thus cannot be shifted by 32
var ret: u64 = heap_hex_slice.len;
ret <<= 32;
ret |= @intFromPtr(heap_hex_slice.ptr);
allocator.free(hash_slice);
return ret;
}
pub fn main() anyerror!void {
// TODO
// if (comptime builtin.cpu.arch == .wasm32) return;
// const args = try std.process.argsAlloc(allocator);
// if (args.len < 3) {
// std.log.err("Usage: zig run src/validator.zig <challenge> <solution>", .{});
// return;
// }
// const challenge = try kCTF.decode(allocator, args[1]);
// defer challenge.destroy(allocator);
// const solution = try kCTF.decode(allocator, args[2]);
// defer solution.destroy(allocator);
// std.log.info("Challenge: {any}\n", .{challenge});
// std.log.info("Solution: {any}\n", .{solution});
// const is_valid = kCTF.check(allocator, challenge, solution) catch |err| {
// std.log.info("Error checking challenge: {s}\n", .{@errorName(err)});
// return;
// };
// std.log.info("Is valid: {}\n", .{is_valid});
}