summaryrefslogtreecommitdiff
path: root/lib/pawprint.zig
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pawprint.zig')
-rw-r--r--lib/pawprint.zig182
1 files changed, 182 insertions, 0 deletions
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,
+ );
+}