From 4afc5758a5bff51b1812c14b8337683aba6c1663 Mon Sep 17 00:00:00 2001 From: Maxwell Beck Date: Mon, 28 Sep 2026 21:44:06 -0500 Subject: initial wip --- flake.lock | 24 ++++++++ flake.nix | 26 ++++++++ lib/.gitignore | 1 + lib/pawprint.zig | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 lib/.gitignore create mode 100644 lib/pawprint.zig diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..64e588c --- /dev/null +++ b/flake.lock @@ -0,0 +1,24 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1787874478, + "narHash": "sha256-mm3axqWht8JL0/6rpAzQBxcJgOrbN4rgkN6s4AjZBOk=", + "rev": "7d1a7c21a4c00ea653fc7ed5c083d261340f185f", + "type": "tarball", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1063175.7d1a7c21a4c0/nixexprs.tar.zst" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.zst" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e618190 --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + inputs = { + nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.zst"; +# rastertype3.url = "git+https://rastertail.net/scm/rastertype3.git"; + }; + + outputs = + { self + , nixpkgs +# , rastertype3 + }: let + system = "aarch64-darwin"; + pkgs = import nixpkgs { + inherit system; +# overlays = [ rastertype3.overlays.default ]; + }; + in { + devShells.${system}.default = let + in pkgs.mkShell { + packages = + [ pkgs.zig pkgs.zls +# pkgs.rastertype3 + ]; + }; + }; +} diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..19892e4 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +.zig-cache diff --git a/lib/pawprint.zig b/lib/pawprint.zig new file mode 100644 index 0000000..8ca4ea7 --- /dev/null +++ b/lib/pawprint.zig @@ -0,0 +1,182 @@ +const std = @import("std"); + +const builtin = std.builtin; +const mem = std.mem; + +const argon2 = std.crypto.pwhash.argon2; +const ed25519 = std.crypto.sign.Ed25519; +const xx32 = std.hash.XxHash32; + +pub const pawprint = struct { + pub const Context = struct { + alloc: mem.Allocator, + io: std.Io, + }; + + pub const identity = struct { + pub const UnlockError = error{ + ChecksumMismatch, + }; + + const salt_len = 16; + + pub fn passphrase_kdf( + ctx: *const Context, + salt: [salt_len]u8, + passphrase: []const u8, + ) ![36]u8 { + var derived: [36]u8 = undefined; + try argon2.kdf( + ctx.alloc, + &derived, + passphrase, + &salt, + argon2.Params.sensitive_2id, + argon2.Mode.argon2id, + ctx.io, + ); + + return derived; + } + + pub const Locked = struct { + bytes: [36]u8, + salt: [salt_len]u8, + + pub fn unlock( + self: *const Locked, + ctx: *const Context, + passphrase: []const u8, + ) !Unlocked { + var seed = try passphrase_kdf(ctx, self.salt, passphrase); + for (0..36) |i| { + seed[i] ^= self.bytes[i]; + } + + const csum = xx32.hash(0, seed[0..32]); + + var csum_bytes: [4]u8 = undefined; + mem.writeInt(u32, &csum_bytes, csum, builtin.Endian.little); + + if (!mem.eql(u8, &csum_bytes, seed[32..36])) { + return UnlockError.ChecksumMismatch; + } + + return Unlocked{ + .keypair = try ed25519.KeyPair.generateDeterministic( + seed[0..32].*, + ), + }; + } + + pub fn drop(self: *volatile Locked) void { + self.* = std.mem.zeroes(Locked); + } + }; + + pub const Unlocked = struct { + keypair: ed25519.KeyPair, + + pub fn generate(io: std.Io) Unlocked { + return Unlocked{ + .keypair = ed25519.KeyPair.generate(io), + }; + } + + pub fn lock( + self: *const Unlocked, + ctx: *const Context, + passphrase: []const u8, + ) !Locked { + var salt: [salt_len]u8 = undefined; + try ctx.io.randomSecure(&salt); + + const derived = try passphrase_kdf(ctx, salt, passphrase); + var locked = Locked{ + .bytes = derived, + .salt = salt, + }; + + const seed = self.keypair.secret_key.seed(); + for (0..32) |i| { + locked.bytes[i] ^= seed[i]; + } + + const csum = xx32.hash(0, seed); + + var csum_bytes: [4]u8 = undefined; + mem.writeInt(u32, &csum_bytes, csum, builtin.Endian.little); + + for (0..4) |i| { + locked.bytes[i + 32] ^= csum_bytes[i]; + } + + return locked; + } + + pub fn drop(self: *volatile Unlocked) void { + self.* = std.mem.zeroes(Unlocked); + } + }; + }; +}; + +test "lock and unlock identity" { + const ctx = pawprint.Context{ + .alloc = std.testing.allocator, + .io = std.testing.io, + }; + + const ident = pawprint.identity.Unlocked.generate(ctx.io); + const locked = try ident.lock(&ctx, "foobar123"); + const unlocked = try locked.unlock(&ctx, "foobar123"); + + try std.testing.expectEqualDeep(ident, unlocked); +} + +test "unlock with wrong passphrase" { + const ctx = pawprint.Context{ + .alloc = std.testing.allocator, + .io = std.testing.io, + }; + + const ident = pawprint.identity.Unlocked.generate(ctx.io); + const locked = try ident.lock(&ctx, "foobar123"); + const unlocked = locked.unlock(&ctx, "barbaz456"); + + try std.testing.expectEqual( + unlocked, + pawprint.identity.UnlockError.ChecksumMismatch, + ); +} + +test "drop unlocked identity" { + const ctx = pawprint.Context{ + .alloc = std.testing.allocator, + .io = std.testing.io, + }; + + var ident = pawprint.identity.Unlocked.generate(ctx.io); + ident.drop(); + + try std.testing.expectEqual( + mem.zeroes(pawprint.identity.Unlocked), + ident, + ); +} + +test "drop locked identity" { + const ctx = pawprint.Context{ + .alloc = std.testing.allocator, + .io = std.testing.io, + }; + + const ident = pawprint.identity.Unlocked.generate(ctx.io); + var locked = try ident.lock(&ctx, "foobar123"); + locked.drop(); + + try std.testing.expectEqual( + mem.zeroes(pawprint.identity.Locked), + locked, + ); +} -- cgit v1.3.1