diff --git a/src/lib/buzz_crypto.zig b/src/lib/buzz_crypto.zig index 16cef50e..3c8ef35e 100644 --- a/src/lib/buzz_crypto.zig +++ b/src/lib/buzz_crypto.zig @@ -97,9 +97,67 @@ pub export fn hash(ctx: *api.NativeCtx) callconv(.c) c_int { return 1; } +pub export fn randomBytes(ctx: *api.NativeCtx) callconv(.c) c_int { + const len_val = ctx.vm.bz_peek(0); + const len = @as(usize, @intCast(len_val.integer())); + const buffer = api.VM.allocator.alloc(u8, len) catch { + ctx.vm.pushError("errors.OutOfMemoryError", null); + return -1; + }; + defer api.VM.allocator.free(buffer); + std.Io.random(ctx.getIo(), buffer); + ctx.vm.bz_push( + api.VM.bz_stringToValue(ctx.vm, buffer.ptr, buffer.len), + ); + return 1; +} + +pub export fn argon2(ctx: *api.NativeCtx) callconv(.c) c_int { + var pass_len: usize = 0; + const pass_ptr = ctx.vm.bz_peek(0).bz_valueToString(&pass_len); + const password = pass_ptr.?[0..pass_len]; + var hash_buf: [256]u8 = undefined; + const io = ctx.getIo(); + const params = std.crypto.pwhash.argon2.Params.owasp_2id; + const hash_r = std.crypto.pwhash.argon2.strHash(password, .{ + .allocator = api.VM.allocator, + .params = params, + }, &hash_buf, io) catch { + ctx.vm.pushError("errors.AuthenticationFailed", "argon2 failed"); + return -1; + }; + ctx.vm.bz_push( + api.VM.bz_stringToValue(ctx.vm, hash_r.ptr, hash_r.len), + ); + + return 1; +} + +pub export fn verifyArgon2(ctx: *api.NativeCtx) callconv(.c) c_int { + var pass_len: usize = 0; + const pass_ptr = ctx.vm.bz_peek(1).bz_valueToString(&pass_len); + const password = pass_ptr.?[0..pass_len]; + + var hash_len: usize = 0; + const hash_ptr = ctx.vm.bz_peek(0).bz_valueToString(&hash_len); + const encoded_hash = hash_ptr.?[0..hash_len]; + + std.crypto.pwhash.argon2.strVerify(encoded_hash, password, .{ .allocator = api.VM.allocator }, ctx.getIo()) catch { + ctx.vm.pushError("errors.AuthenticationFailed", "argon2 verification failed"); + return -1; + }; + + ctx.vm.bz_push(.True); + + return 1; +} + pub const library = api.BuzzApi( "crypto", &.{ &.{ "hash", hash }, + &.{ "randomBytes", randomBytes }, + &.{ "argon2", argon2 }, + &.{ "verifyArgon2", verifyArgon2 }, }, ){}; diff --git a/src/lib/crypto.buzz b/src/lib/crypto.buzz index d1210ec8..f4865d46 100644 --- a/src/lib/crypto.buzz +++ b/src/lib/crypto.buzz @@ -1,5 +1,7 @@ namespace crypto; +import "buzz:errors"; + /// Hash algorithms export enum HashAlgorithm { Md5, @@ -22,3 +24,20 @@ export enum HashAlgorithm { /// @param data Data to hash /// @return Hash of data has hex string export extern fun hash(algo: HashAlgorithm, data: str) > str; + +/// Generate Cryptographically secure randombytes +/// @param len Number of random bytes to generate +/// @return randombytes as string +export extern fun randomBytes(len: int) > str; + + +/// Hash a password using Argon2id +/// @param password The password to hash +/// @return The encoded hash string +export extern fun argon2(password: str) > str !> errors\AuthenticationFailed; + +/// Verify a password against an Argon2id hash +/// @param password The password to verify +/// @param hash The encoded hash string from argon2id() +/// @return true if the password matches the hash +export extern fun verifyArgon2(password: str, hash: str) > bool !> errors\AuthenticationFailed; diff --git a/src/lib/errors.buzz b/src/lib/errors.buzz index 413b84f3..6df0ea05 100644 --- a/src/lib/errors.buzz +++ b/src/lib/errors.buzz @@ -187,3 +187,8 @@ export object UnderflowError { export object UnexpectedError { message: str = "UnexpectedError", } + +/// Error raised when an authentication process failed +export object AuthenticationFailed { + message: str = "AuthenticationFailed", +} diff --git a/src/lib/static_libraries.zig b/src/lib/static_libraries.zig index 6463a3c5..2d1db335 100644 --- a/src/lib/static_libraries.zig +++ b/src/lib/static_libraries.zig @@ -17,7 +17,7 @@ pub const Library = struct { /// Libraries bundled with the compiler and runtime. pub const all = [_]Library{ .{ .header = static_headers.buffer, .zig_path = "buzz_buffer.zig", .wasm_native = true }, - .{ .header = static_headers.crypto, .zig_path = "buzz_crypto.zig", .wasm_native = true }, + .{ .header = static_headers.crypto, .zig_path = "buzz_crypto.zig", .wasm_native = false }, .{ .header = static_headers.debug, .zig_path = "buzz_debug.zig", .wasm_native = true }, .{ .header = static_headers.errors, .zig_path = null, .wasm_native = false }, .{ .header = static_headers.ffi, .zig_path = "buzz_ffi.zig", .wasm_native = false }, diff --git a/tests/behavior/crypto.buzz b/tests/behavior/crypto.buzz index 2b00bb11..ceb1a4a1 100644 --- a/tests/behavior/crypto.buzz +++ b/tests/behavior/crypto.buzz @@ -28,3 +28,66 @@ test "hash" { message: "sha3-256", ); } + +test "randomBytes returns correct length" { + final len = 16; + final bytes = randomBytes(len); + std\assert( + bytes.len() == len, + message: "randomBytes should return a string of the requested length" + ); +} + +test "randomBytes returns different values" { + final b1 = randomBytes(8); + final b2 = randomBytes(8); + std\assert( + b1 != b2, + message: "randomBytes should return different values" + ); +} + +test "argon2 hashes and verifies" { + final password = "my-secure-password"; + final hash = argon2(password) catch from { + std\assert( + false, + message: "argon2 failed" + ); + return; + }; + final valid = verifyArgon2(password, hash) catch from { + std\assert( + false, + message: "verifyArgon2 failed" + ); + return; + }; + std\assert( + valid, + message: "argon2 verification should succeed for correct password" + ); +} + +test "argon2 fails for wrong password" { + final password = "my-secure-password"; + final wrong = "wrong-password"; + final hash = argon2(password) catch from { + std\assert( + false, + message: "argon2 failed" + ); + return; + }; + final valid = verifyArgon2(wrong, hash) catch from { + std\assert( + false, + message: "verifyArgon2 failed" + ); + return; + }; + std\assert( + !valid, + message: "argon2 verification should fail for wrong password" + ); +}