summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxwell Beck <max@rastertail.net>2026-09-28 21:44:06 -0500
committerMaxwell Beck <max@rastertail.net>2026-09-28 21:44:06 -0500
commit4afc5758a5bff51b1812c14b8337683aba6c1663 (patch)
tree522a49ce21709f2a789889c89ddc50b28f01cc24
parent92ec8a8c6662051674f9973749f61c939721e7cc (diff)
initial wipdev
-rw-r--r--flake.lock24
-rw-r--r--flake.nix26
-rw-r--r--lib/.gitignore1
-rw-r--r--lib/pawprint.zig182
4 files changed, 233 insertions, 0 deletions
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,
+ );
+}