diff --git a/build.zig b/build.zig index 0c9d64b..ff5cbe4 100644 --- a/build.zig +++ b/build.zig @@ -14,18 +14,17 @@ pub fn build(b: *std.build.Builder) !void { }); const zbor_module = zbor_dep.module("zbor"); - //const hidapi_dep = b.dependency("hidapi", .{ - // .target = target, - // .optimize = optimize, - //}); - //_ = hidapi_dep; + const uuid_dep = b.dependency("uuid", .{ + .target = target, + .optimize = optimize, + }); + const uuid_module = uuid_dep.module("uuid"); - //const clap_dep = b.dependency("clap", .{ - // .target = target, - // .optimize = optimize, - //}); - //const clap_module = clap_dep.module("clap"); - //_ = clap_module; + const snorlax_dep = b.dependency("snorlax", .{ + .target = target, + .optimize = optimize, + }); + const snorlax_module = snorlax_dep.module("snorlax"); // ++++++++++++++++++++++++++++++++++++++++++++ // Module @@ -43,6 +42,18 @@ pub fn build(b: *std.build.Builder) !void { try b.modules.put(b.dupe("cks"), cks_module); + // Allocator Module + // ------------------------------------------------ + + const allocator_module = b.addModule("cks", .{ + .source_file = .{ .path = "profiling_allocator/main.zig" }, + .dependencies = &.{ + .{ .name = "profiling_allocator", .module = zbor_module }, + }, + }); + + try b.modules.put(b.dupe("profiling_allocator"), allocator_module); + // Authenticator Module // ------------------------------------------------ @@ -50,6 +61,7 @@ pub fn build(b: *std.build.Builder) !void { .source_file = .{ .path = "lib/main.zig" }, .dependencies = &.{ .{ .name = "zbor", .module = zbor_module }, + .{ .name = "uuid", .module = uuid_module }, .{ .name = "cks", .module = cks_module }, }, }); @@ -68,6 +80,8 @@ pub fn build(b: *std.build.Builder) !void { }); authenticator.addModule("fido", fido_module); authenticator.addModule("cks", cks_module); + authenticator.addModule("profiling_allocator", allocator_module); + authenticator.addModule("snorlax", snorlax_module); authenticator.linkSystemLibraryPkgConfigOnly("libnotify"); authenticator.linkLibC(); b.installArtifact(authenticator); diff --git a/build.zig.zon b/build.zig.zon index 284f1de..45fd963 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +4,16 @@ .dependencies = .{ .zbor = .{ - .url = "https://github.com/r4gus/zbor/archive/master.tar.gz", - .hash = "1220a1b2ab5f3092f4f29d85b0627cc6adc54c973ae588ddc75e630b624293ea4722", + .url = "https://github.com/r4gus/zbor/archive/refs/tags/0.11.0.tar.gz", + .hash = "12204bf1073e9bc1eb12d09a2c298c687ed9c782c687f98ccda3bc95daad8c216845", + }, + .uuid = .{ + .url = "https://github.com/r4gus/uuid-zig/archive/refs/tags/0.1.0.tar.gz", + .hash = "1220b32a616236fd26f0e4f063f3f11f5c5b83943d1fd71c367cf0bdbebedbf42385", + }, + .snorlax = .{ + .url = "https://github.com/r4gus/snorlax/archive/master.tar.gz", + .hash = "1220911c2a1f74b601affd25af9c9e32854fda1ab6b9205db01dc63e0e3ba92df919", }, }, } diff --git a/lib/common/AttestationStatement.zig b/lib/common/AttestationStatement.zig index 14a80ac..c2d4c38 100644 --- a/lib/common/AttestationStatement.zig +++ b/lib/common/AttestationStatement.zig @@ -33,4 +33,13 @@ pub const AttestationStatement = union(fido.common.AttestationStatementFormatIde /// receive attestation information, see § 5.4.7 Attestation Conveyance Preference /// Enumeration (enum AttestationConveyancePreference). none: struct {}, // no attestation + + pub fn deinit(self: *const @This(), a: std.mem.Allocator) void { + switch (self.*) { + .@"packed" => |x| { + a.free(x.sig); + }, + else => {}, + } + } }; diff --git a/lib/ctap/auth/Authenticator.zig b/lib/ctap/auth/Authenticator.zig index 97ffdb3..8c91a2e 100644 --- a/lib/ctap/auth/Authenticator.zig +++ b/lib/ctap/auth/Authenticator.zig @@ -29,39 +29,106 @@ token: struct { }, credential_list: ?struct { - list: []const fido.ctap.crypto.Id, + list: []fido.ctap.authenticator.Credential, credentialCounter: usize = 0, time_stamp: i64, authData: fido.common.AuthenticatorData = undefined, clientDataHash: fido.ctap.crypto.ClientDataHash = undefined, pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + for (self.list) |item| { + item.deinit(allocator); + } allocator.free(self.list); } } = null, +cred_mngmnt: ?struct { + ids: std.ArrayList([]const u8), + time_stamp: i64, + prot: fido.ctap.pinuv.common.PinProtocol, + token: [32]u8, + + pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { + for (self.ids.items) |item| { + allocator.free(item); + } + self.ids.deinit(); + } +} = null, + allocator: std.mem.Allocator, -pub fn init(self: *@This()) !void { - var settings = if (self.callbacks.getEntry("Settings")) |settings| settings else blk: { - std.log.warn("No Settings entry found", .{}); - - var s = try self.callbacks.createEntry("Settings"); - try s.addField(.{ .key = "Retries", .value = "\x08" }, std.time.milliTimestamp()); - try s.addField(.{ .key = "ForcePinChange", .value = "False" }, std.time.milliTimestamp()); - try s.addField(.{ .key = "MinPinLength", .value = "\x04" }, std.time.milliTimestamp()); - try s.addField(.{ .key = "AlwaysUv", .value = "True" }, std.time.milliTimestamp()); - try s.addField(.{ - .key = "Secret", - .value = &fido.ctap.crypto.master_secret.createMasterSecret(self.callbacks.rand), - }, self.callbacks.millis()); - try self.callbacks.addEntry(s); - - break :blk self.callbacks.getEntry("Settings").?; +/// TODO: We use AES256-OCB and HMAC. It should be fine to use the same key +/// for HMAC and AES256-OCB encryption but maybe its still better to use +/// two different keys. +secret: fido.ctap.authenticator.Meta.Keys = undefined, + +/// Initialize the authenticator +/// +/// This will try to load dynamic authenticator settings, i.e. those +/// that will change over time. +/// +/// The settings will be created, if they don't exist. This includes a new +/// master secret, used to encrypt all credentials. The master secret itself +/// is encrypted using the provided password (a key derived from the password +/// to be more precise). +pub fn init(self: *@This(), password: []const u8) !void { + var new: bool = false; + var setting = self.callbacks.readSettings(self.allocator) catch |err| blk: { + if (err == error.DoesNotExist) { + std.log.warn("No Settings entry found", .{}); + var meta = fido.ctap.authenticator.Meta{ + ._id = try self.allocator.dupe(u8, "Settings"), + }; + + // First we derive a key from the given password. + // This will also generate a random salt that is stored with + // the rest of the meta data and used for the key derivation. + // + // From now on we can call meta.deriveKey(password) to derive the key. + self.secret = try meta.newKey(password, self.callbacks.rand, self.allocator); + + // Lets create a new master secret. This is used to encrypt all generated credentials. + // We use this indirection so we don't need to re-encrypt all credentials if the user + // changes the password. + const ms = fido.ctap.crypto.master_secret.createMasterSecret(self.callbacks.rand); + meta.setSecret(ms, self.secret.enc, self.callbacks.rand); + + // Finally, we calculate a mac over the data. The master secret uses authenticated encryption + // but I don't want to reencrypt the master secret every time one of the other fields changes. + meta.updateMac(&self.secret.mac); + + self.callbacks.updateSettings(&meta, self.allocator) catch |e| { + std.log.err("unable to persist new settings ({any})", .{e}); + return error.Fatal; + }; + + new = true; + break :blk meta; + } else { + std.log.err("fatal error while loading settings", .{}); + return error.Fatal; + } }; - _ = settings; + defer setting.deinit(self.allocator); + + if (!new) { + self.secret = try setting.deriveKey(password, self.allocator); + } + + if (!setting.verifyMac(&self.secret.mac)) { + std.log.err("MAC verification for the given settings failed", .{}); + return error.Fatal; + } - try self.callbacks.persist(); + if (self.token.one) |*one| { + one.initialize(); + } + + if (self.token.two) |*two| { + two.initialize(); + } } pub fn deinit(self: *@This()) void { @@ -69,6 +136,10 @@ pub fn deinit(self: *@This()) void { self.credential_list.?.deinit(self.allocator); self.credential_list = null; } + if (self.cred_mngmnt != null) { + self.cred_mngmnt.?.deinit(self.allocator); + self.cred_mngmnt = null; + } } pub fn handle(self: *@This(), command: []const u8) Response { diff --git a/lib/ctap/auth/Callbacks.zig b/lib/ctap/auth/Callbacks.zig index 7b1d499..c42e7db 100644 --- a/lib/ctap/auth/Callbacks.zig +++ b/lib/ctap/auth/Callbacks.zig @@ -9,11 +9,14 @@ const cks = @import("cks"); pub const LoadError = error{ DoesNotExist, - NotEnoughMemory, + OutOfMemory, + Other, }; pub const StoreError = error{ KeyStoreFull, + OutOfMemory, + Other, }; /// Result value of the `up` callback @@ -40,6 +43,13 @@ pub const CredentialSelectionResult = union(CredentialSelectionResultTag) { timeout: void, }; +pub const ReadCredParamId = enum { id, rpId, all }; +pub const ReadCredParam = union(ReadCredParamId) { + id: []const u8, + rpId: []const u8, + all: bool, +}; + /// Interface for a thread local CSPRNG rand: std.rand.Random, @@ -68,12 +78,12 @@ select_discoverable_credential: ?*const fn ( users: []const fido.common.User, ) CredentialSelectionResult = null, -createEntry: *const fn (id: []const u8) cks.Error!cks.Entry, -getEntry: *const fn (id: []const u8) ?*cks.Entry, -getEntries: *const fn (filters: []const cks.Data.Filter, a: std.mem.Allocator) ?[]const *cks.Entry, -addEntry: *const fn (entry: cks.Entry) cks.Error!void, -removeEntry: *const fn (id: []const u8) cks.Error!void, -persist: *const fn () error{Fatal}!void, +readSettings: *const fn (a: std.mem.Allocator) LoadError!fido.ctap.authenticator.Meta, +updateSettings: *const fn (settings: *fido.ctap.authenticator.Meta, a: std.mem.Allocator) StoreError!void, + +readCred: *const fn (id: ReadCredParam, a: std.mem.Allocator) LoadError![]fido.ctap.authenticator.Credential, +updateCred: *const fn (cred: *fido.ctap.authenticator.Credential, a: std.mem.Allocator) StoreError!void, +deleteCred: *const fn (cred: *fido.ctap.authenticator.Credential) LoadError!void, //// Called on a reset //// diff --git a/lib/ctap/auth/Credential.zig b/lib/ctap/auth/Credential.zig new file mode 100644 index 0000000..5aa7d49 --- /dev/null +++ b/lib/ctap/auth/Credential.zig @@ -0,0 +1,227 @@ +const std = @import("std"); +const fido = @import("../../main.zig"); +const cbor = @import("zbor"); +const uuid = @import("uuid"); + +const Mac = std.crypto.auth.hmac.sha2.HmacSha256; +const Aes256Ocb = std.crypto.aead.aes_ocb.Aes256Ocb; + +/// Credential ID +_id: []const u8, + +/// Revision (can be ignored for the most part) +_rev: ?[]const u8 = null, + +user_id: []const u8, + +user_name: ?[]const u8 = null, + +user_display_name: ?[]const u8 = null, + +/// The ID of the Relying Party (usually a base URL) +rp_id: []const u8, + +/// Number of signatures issued using the given credential +sign_count: u64, + +/// Signature algorithm to use for the credential +alg: cbor.cose.Algorithm, + +/// The AES-OCB encrypted private key +private_key: []const u8 = undefined, + +policy: fido.ctap.extensions.CredentialCreationPolicy = .userVerificationOptional, + +/// Belongs to hmac secret +cred_random_with_uv: [32]u8 = undefined, + +/// Belongs to hmac secret +cred_random_without_uv: [32]u8 = undefined, + +/// Is this credential discoverable or not +/// +/// This is kind of stupid but authenticatorMakeCredential +/// docs state, that you're not allowed to create a discoverable +/// credential if not explicitely requested. The docs also state +/// that you're allowed to keep (some) state, e.g., store the key. +discoverable: bool = false, + +/// Message Authentication Code over the remaining data +mac: [Mac.mac_length]u8 = undefined, + +pub fn copy(self: *const @This(), a: std.mem.Allocator) !@This() { + return .{ + ._id = try a.dupe(u8, self._id), + ._rev = if (self._rev) |rev| try a.dupe(u8, rev) else null, + .user_id = try a.dupe(u8, self.user_id), + .user_name = if (self.user_name) |name| try a.dupe(u8, name) else null, + .user_display_name = if (self.user_display_name) |name| try a.dupe(u8, name) else null, + .rp_id = try a.dupe(u8, self.rp_id), + .sign_count = self.sign_count, + .alg = self.alg, + .private_key = try a.dupe(u8, self.private_key), + .policy = self.policy, + .cred_random_with_uv = self.cred_random_with_uv, + .cred_random_without_uv = self.cred_random_without_uv, + .discoverable = self.discoverable, + .mac = self.mac, + }; +} + +pub fn allocInit( + id: uuid.Uuid, + user: *const fido.common.User, + rp_id: []const u8, + alg: cbor.cose.Algorithm, + policy: fido.ctap.extensions.CredentialCreationPolicy, + allocator: std.mem.Allocator, + rand: std.rand.Random, +) !@This() { + const urn = uuid.urn.serialize(id); + var self = @This(){ + ._id = try allocator.dupe(u8, urn[0..]), + .user_id = try allocator.dupe(u8, user.id), + .rp_id = try allocator.dupe(u8, rp_id), + .sign_count = 0, + .alg = alg, + .policy = policy, + }; + + if (user.name) |name| { + self.user_name = try allocator.dupe(u8, name); + } + if (user.displayName) |name| { + self.user_display_name = try allocator.dupe(u8, name); + } + + rand.bytes(self.cred_random_with_uv[0..]); + rand.bytes(self.cred_random_without_uv[0..]); + + self.sign_count = 0; + + return self; +} + +pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { + allocator.free(self._id); + if (self._rev) |rev| { + allocator.free(rev); + } + allocator.free(self.user_id); + if (self.user_name) |name| { + allocator.free(name); + } + if (self.user_display_name) |name| { + allocator.free(name); + } + allocator.free(self.rp_id); + allocator.free(self.private_key); +} + +pub fn setPrivateKey( + self: *@This(), + private_key: []const u8, + key: [Aes256Ocb.key_length]u8, + rand: std.rand.Random, + allocator: std.mem.Allocator, +) !void { + var m = try allocator.alloc(u8, Aes256Ocb.nonce_length + Aes256Ocb.tag_length + private_key.len); + rand.bytes(m[0..Aes256Ocb.nonce_length]); + Aes256Ocb.encrypt( + m[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + m[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length], + private_key[0..], + "", + m[0..Aes256Ocb.nonce_length].*, + key, + ); + self.private_key = m; +} + +pub fn getPrivateKey( + self: *const @This(), + key: [Aes256Ocb.key_length]u8, + allocator: std.mem.Allocator, +) ![]const u8 { + var m = try allocator.alloc(u8, self.private_key.len - Aes256Ocb.nonce_length - Aes256Ocb.tag_length); + try Aes256Ocb.decrypt( + m, + self.private_key[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + self.private_key[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length].*, + "", + self.private_key[0..Aes256Ocb.nonce_length].*, + key, + ); + + return m; +} + +pub fn updateMac(self: *@This(), key: []const u8) void { + var m = Mac.init(key); + m.update(self._id); + m.update(self.user_id); + m.update(self.rp_id); + m.update(std.mem.asBytes(&self.sign_count)); + m.update(std.mem.asBytes(&self.alg)); + m.update(self.private_key); + m.update(std.mem.asBytes(&self.policy)); + m.update(&self.cred_random_with_uv); + m.update(&self.cred_random_without_uv); + m.update(std.mem.asBytes(&self.discoverable)); + m.final(&self.mac); +} + +pub fn verifyMac(self: *@This(), key: []const u8) bool { + var x: [Mac.mac_length]u8 = undefined; + var m = Mac.init(key); + m.update(self._id); + m.update(self.user_id); + m.update(self.rp_id); + m.update(std.mem.asBytes(&self.sign_count)); + m.update(std.mem.asBytes(&self.alg)); + m.update(self.private_key); + m.update(std.mem.asBytes(&self.policy)); + m.update(&self.cred_random_with_uv); + m.update(&self.cred_random_without_uv); + m.update(std.mem.asBytes(&self.discoverable)); + m.final(&x); + + return std.mem.eql(u8, x[0..], self.mac[0..]); +} + +test "Credential mac #1" { + const k = "\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07"; + + var x = @This(){ + ._id = "credential1", + .user_id = "12345", + .rp_id = "github.com", + .sign_count = 89, + .alg = .Es256, + .private_key = "privatekey", + }; + x.updateMac(k); + try std.testing.expectEqual(true, x.verifyMac(k)); + + x.sign_count += 1; + try std.testing.expectEqual(false, x.verifyMac(k)); +} + +test "Credential encrypt/decrypt #1" { + const allocator = std.testing.allocator; + const k = "\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07"; + + var x = @This(){ + ._id = "credential1", + .user_id = "12345", + .rp_id = "github.com", + .sign_count = 89, + .alg = .Es256, + }; + try x.setPrivateKey("privatekey", k.*, std.crypto.random, allocator); + defer allocator.free(x.private_key); + const m = try x.getPrivateKey(k.*, allocator); + defer allocator.free(m); + + try std.testing.expectEqualSlices(u8, "privatekey", m); +} diff --git a/lib/ctap/auth/Meta.zig b/lib/ctap/auth/Meta.zig new file mode 100644 index 0000000..899c8b3 --- /dev/null +++ b/lib/ctap/auth/Meta.zig @@ -0,0 +1,222 @@ +const std = @import("std"); +const fido = @import("../../main.zig"); + +const Mac = std.crypto.auth.hmac.sha2.HmacSha256; +const Aes256Ocb = std.crypto.aead.aes_ocb.Aes256Ocb; +const argon2 = std.crypto.pwhash.argon2; +const master_secret = fido.ctap.crypto.master_secret; + +pub const KEY_LEN = Aes256Ocb.key_length; + +pub const Keys = struct { + mac: [KEY_LEN]u8, + enc: [KEY_LEN]u8, +}; + +_id: []const u8 = "Settings", +_rev: ?[]const u8 = null, +/// Number of retries left +retries: u8 = 8, +/// Pin has to be changed +force_pin_change: bool = false, +/// The minimum pin length +min_pin_length: u8 = 4, +/// Enforce user verification +always_uv: bool = true, +/// Master secret encrypted using AES-OCB +secret: [Aes256Ocb.nonce_length + Aes256Ocb.tag_length + master_secret.MS_LEN]u8 = undefined, +/// Pin with a max length of 63 bytes +pin: ?[Aes256Ocb.nonce_length + Aes256Ocb.tag_length + 33]u8 = null, +/// Gloabl credential usage counter +usage_count: u64 = 0, +/// Message Authentication Code over the remaining data +mac: [Mac.mac_length]u8 = undefined, +kdf: struct { + salt: [8]u8 = undefined, + // recommendation by OWASP + P: u24 = 1, + M: u32 = 7168, + I: u32 = 5, +} = .{}, + +pub fn deinit(self: *const @This(), a: std.mem.Allocator) void { + a.free(self._id); + if (self._rev) |rev| { + a.free(rev); + } +} + +pub fn newKey( + self: *@This(), + password: []const u8, + random: std.rand.Random, + a: std.mem.Allocator, +) !Keys { + random.bytes(self.kdf.salt[0..]); + return try self.deriveKey(password, a); +} + +pub fn deriveKey( + self: *@This(), + password: []const u8, + a: std.mem.Allocator, +) !Keys { + var k: [KEY_LEN + KEY_LEN]u8 = undefined; + try argon2.kdf( + a, + k[0..], + password, + self.kdf.salt[0..], + .{ .t = self.kdf.I, .m = self.kdf.M, .p = self.kdf.P }, + .argon2id, + ); + return Keys{ + .mac = k[0..KEY_LEN].*, + .enc = k[KEY_LEN..].*, + }; +} + +pub fn updateMac(self: *@This(), key: []const u8) void { + var m = Mac.init(key); + m.update(self._id); + m.update(std.mem.asBytes(&self.retries)); + m.update(std.mem.asBytes(&self.force_pin_change)); + m.update(std.mem.asBytes(&self.min_pin_length)); + m.update(std.mem.asBytes(&self.always_uv)); + m.update(&self.secret); + if (self.pin) |pin| { + m.update(&pin); + } + m.update(std.mem.asBytes(&self.usage_count)); + m.final(&self.mac); +} + +pub fn verifyMac(self: *@This(), key: []const u8) bool { + var x: [Mac.mac_length]u8 = undefined; + var m = Mac.init(key); + m.update(self._id); + m.update(std.mem.asBytes(&self.retries)); + m.update(std.mem.asBytes(&self.force_pin_change)); + m.update(std.mem.asBytes(&self.min_pin_length)); + m.update(std.mem.asBytes(&self.always_uv)); + m.update(&self.secret); + if (self.pin) |pin| { + m.update(&pin); + } + m.update(std.mem.asBytes(&self.usage_count)); + m.final(&x); + + return std.mem.eql(u8, x[0..], self.mac[0..]); +} + +pub fn setSecret( + self: *@This(), + secret: master_secret.MasterSecret, + key: [Aes256Ocb.key_length]u8, + rand: std.rand.Random, +) void { + rand.bytes(self.secret[0..Aes256Ocb.nonce_length]); + Aes256Ocb.encrypt( + self.secret[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + self.secret[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length], + secret[0..], + "", + self.secret[0..Aes256Ocb.nonce_length].*, + key, + ); +} + +pub fn getSecret( + self: *const @This(), + key: [Aes256Ocb.key_length]u8, +) !master_secret.MasterSecret { + var m: master_secret.MasterSecret = undefined; + + try Aes256Ocb.decrypt( + &m, + self.secret[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + self.secret[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length].*, + "", + self.secret[0..Aes256Ocb.nonce_length].*, + key, + ); + + return m; +} + +pub fn setPin( + self: *@This(), + pin: [32]u8, + code_points: u8, + key: [Aes256Ocb.key_length]u8, + rand: std.rand.Random, +) !void { + var p: [33]u8 = .{0} ** 33; + @memcpy(p[0..32], pin[0..32]); + p[32] = code_points; + + self.pin = .{0} ** (Aes256Ocb.nonce_length + Aes256Ocb.tag_length + 33); + rand.bytes(self.pin.?[0..Aes256Ocb.nonce_length]); + Aes256Ocb.encrypt( + self.pin.?[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + self.pin.?[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length], + p[0..], + "", + self.pin.?[0..Aes256Ocb.nonce_length].*, + key, + ); +} + +pub fn getPin( + self: *const @This(), + key: [Aes256Ocb.key_length]u8, + code_points: *u8, +) ![32]u8 { + if (self.pin == null) return error.NoPinSet; + + var m: [33]u8 = undefined; + + try Aes256Ocb.decrypt( + &m, + self.pin.?[Aes256Ocb.nonce_length + Aes256Ocb.tag_length ..], + self.pin.?[Aes256Ocb.nonce_length .. Aes256Ocb.nonce_length + Aes256Ocb.tag_length].*, + "", + self.pin.?[0..Aes256Ocb.nonce_length].*, + key, + ); + + code_points.* = m[32]; + return m[0..32].*; +} + +test "Meta mac #1" { + const k = "\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07"; + const secret: [32]u8 = "012345679abcdefg012345679abcdefg".*; + + var x = @This(){}; + x.setSecret(secret, k.*, std.crypto.random); + x.updateMac(k); + + try std.testing.expectEqual(true, x.verifyMac(k)); + + x.retries -= 1; + try std.testing.expectEqual(false, x.verifyMac(k)); + + x.retries += 1; + x.force_pin_change = true; + try std.testing.expectEqual(false, x.verifyMac(k)); + + x.force_pin_change = false; + try std.testing.expectEqual(true, x.verifyMac(k)); +} + +test "Meta encrypt/decrypt #1" { + const k = "\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07\x00\x01\x02\x03\x04\x05\x06\x07"; + const secret: [32]u8 = "012345679abcdefg012345679abcdefg".*; + + var x = @This(){}; + x.setSecret(secret, k.*, std.crypto.random); + const m = try x.getSecret(k.*); + + try std.testing.expectEqualSlices(u8, "012345679abcdefg012345679abcdefg", &m); +} diff --git a/lib/ctap/commands/authenticator/authenticatorClientPin.zig b/lib/ctap/commands/authenticator/authenticatorClientPin.zig index ba8db85..c5d12e3 100644 --- a/lib/ctap/commands/authenticator/authenticatorClientPin.zig +++ b/lib/ctap/commands/authenticator/authenticatorClientPin.zig @@ -24,20 +24,21 @@ pub fn authenticatorClientPin( var client_pin_response: ?fido.ctap.response.ClientPin = null; - var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { - std.log.err("Unable to fetch Settings", .{}); + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("authenticatorClientPin: Unable to fetch Settings ({any})", .{err}); return fido.ctap.StatusCodes.ctap1_err_other; }; + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("authenticatorClientPin: Settings MAC validation unsuccessful", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + } // Handle one of the sub-commands switch (client_pin_param.subCommand) { .getPinRetries => { - var retries = if (settings.getField("Retries", auth.callbacks.millis())) |retries| retries else { - std.log.err("Retries field missing in Settings", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - }; client_pin_response = .{ - .pinRetries = retries[0], + .pinRetries = settings.retries, .powerCycleState = retry_state.powerCycleState, }; }, @@ -82,8 +83,7 @@ pub fn authenticatorClientPin( .V2 => &auth.token.two.?, }; - var already_set = if (settings.getField("Pin", auth.callbacks.millis())) |_| true else false; - + var already_set = settings.pin != null; if (already_set) { return fido.ctap.StatusCodes.ctap2_err_pin_auth_invalid; } @@ -144,9 +144,12 @@ pub fn authenticatorClientPin( // Store new pin const ph = fido.ctap.pinuv.hash(newPin); - try settings.addField(.{ .key = "Pin", .value = &ph }, auth.callbacks.millis()); - try settings.addField(.{ .key = "CodePoints", .value = std.mem.toBytes(code_points)[0..] }, auth.callbacks.millis()); - try auth.callbacks.persist(); + try settings.setPin(ph, code_points, auth.secret.enc, auth.callbacks.rand); + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("authenticatorClientPin (setPin): unable to update settings ({any})", .{err}); + return err; + }; }, .changePIN => { if (retry_state.ctr == 0) { @@ -175,11 +178,7 @@ pub fn authenticatorClientPin( }; // If the pinRetries counter is 0, return error. - var retries: u8 = if (settings.getField("Retries", auth.callbacks.millis())) |retries| retries[0] else { - std.log.err("changePIN: Retries field missing in Settings", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - }; - if (retries <= 0) { + if (settings.retries <= 0) { return fido.ctap.StatusCodes.ctap2_err_pin_blocked; } @@ -211,12 +210,12 @@ pub fn authenticatorClientPin( } // decrement pin retries - retries = retries - 1; - try settings.updateField( - "Retries", - &std.mem.toBytes(retries), - auth.callbacks.millis(), - ); + settings.retries -= 1; + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("authenticatorClientPin (updatePin): unable to update settings ({any})", .{err}); + return err; + }; // Decrypt pinHashEnc and match against stored pinHash var pinHash1: [16]u8 = undefined; @@ -226,7 +225,12 @@ pub fn authenticatorClientPin( client_pin_param.pinHashEnc.?[0..], ); - const pinHash2 = if (settings.getField("Pin", auth.callbacks.millis())) |pin| pin else return fido.ctap.StatusCodes.ctap2_err_pin_not_set; + if (settings.pin == null) { + return fido.ctap.StatusCodes.ctap2_err_pin_not_set; + } + + var cp: u8 = 0; + const pinHash2 = try settings.getPin(auth.secret.enc, &cp); if (!std.mem.eql(u8, pinHash1[0..], pinHash2[0..16])) { // The pin hashes don't match @@ -234,7 +238,7 @@ pub fn authenticatorClientPin( prot.regenerate(); - if (retries == 0) { + if (settings.retries == 0) { return fido.ctap.StatusCodes.ctap2_err_pin_blocked; } else if (retry_state.ctr == 0) { return fido.ctap.StatusCodes.ctap2_err_pin_auth_blocked; @@ -244,12 +248,12 @@ pub fn authenticatorClientPin( } // Set the pinRetries to maximum - retries = 8; - try settings.updateField( - "Retries", - &std.mem.toBytes(retries), - auth.callbacks.millis(), - ); + settings.retries = 8; + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("changePIN: unable to update settings ({any})", .{err}); + return err; + }; // Decrypt new pin var paddedNewPin: [64]u8 = undefined; @@ -280,9 +284,9 @@ pub fn authenticatorClientPin( const ph = fido.ctap.pinuv.hash(newPin); // Validate forePINChange - if (auth.settings.forcePINChange) |fpc| { + if (settings.force_pin_change) { // Hash of new pin must not be the same as the old hash - if (fpc and std.mem.eql(u8, pinHash2[0..], &ph)) { + if (std.mem.eql(u8, pinHash2[0..], &ph)) { std.log.err("authenticatorClientPin (changePin): new and old pin must differ", .{}); return fido.ctap.StatusCodes.ctap2_err_pin_policy_violation; } @@ -296,19 +300,13 @@ pub fn authenticatorClientPin( } } - try settings.updateField( - "Pin", - &ph, - auth.callbacks.millis(), - ); - - try settings.updateField( - "CodePoints", - std.mem.toBytes(code_points)[0..], - auth.callbacks.millis(), - ); - try auth.callbacks.persist(); - auth.settings.forcePINChange = false; + try settings.setPin(ph, code_points, auth.secret.enc, auth.callbacks.rand); + settings.force_pin_change = false; + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("authenticatorClientPin (changePin): unable to update settings ({any})", .{err}); + return err; + }; // Invalidate all pinUvAuthTokens if (auth.token.one) |*one| { @@ -357,11 +355,7 @@ pub fn authenticatorClientPin( } // Check if the pin is blocked - var retries: u8 = if (settings.getField("Retries", auth.callbacks.millis())) |retries| retries[0] else { - std.log.err("getUvTokenWithPerm: Retries field missing in Settings", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - }; - if (retries == 0) { + if (settings.retries == 0) { return fido.ctap.StatusCodes.ctap2_err_pin_blocked; } @@ -375,12 +369,12 @@ pub fn authenticatorClientPin( defer auth.allocator.free(shared_secret); // decrement pin retries - retries = retries - 1; - try settings.updateField( - "Retries", - &std.mem.toBytes(retries), - auth.callbacks.millis(), - ); + settings.retries -= 1; + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("getPinUvAuthTokenUsingPinWithPermissions: unable to update settings ({any})", .{err}); + return err; + }; // Decrypt pinHashEnc and match against stored pinHash var pinHash1: [16]u8 = undefined; @@ -390,10 +384,8 @@ pub fn authenticatorClientPin( client_pin_param.pinHashEnc.?[0..], ); - var pinHash2 = if (settings.getField("Pin", auth.callbacks.millis())) |pin| pin else { - std.log.err("getUvTokenWithPerm: Pin field missing in Settings", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - }; + var cp: u8 = 0; + const pinHash2 = try settings.getPin(auth.secret.enc, &cp); if (!std.mem.eql(u8, pinHash1[0..], pinHash2[0..16])) { // The pin hashes don't match @@ -401,7 +393,7 @@ pub fn authenticatorClientPin( prot.regenerate(); - if (retries == 0) { + if (settings.retries == 0) { return fido.ctap.StatusCodes.ctap2_err_pin_blocked; } else if (retry_state.ctr == 0) { return fido.ctap.StatusCodes.ctap2_err_pin_auth_blocked; @@ -411,20 +403,17 @@ pub fn authenticatorClientPin( } // Set retry counter to maximum - retries = 8; - try settings.updateField( - "Retries", - &std.mem.toBytes(retries), - auth.callbacks.millis(), - ); - try auth.callbacks.persist(); + settings.retries = 8; + settings.updateMac(&auth.secret.mac); + auth.callbacks.updateSettings(&settings, auth.allocator) catch |err| { + std.log.err("getPinUvAuthTokenUsingPinWithPermissions: unable to update settings ({any})", .{err}); + return err; + }; // Check if user is forced to change the pin - if (auth.settings.forcePINChange) |change| { - if (change) { - std.log.err("authenticatorClientPin (getPinUvAuthTokenUsingPinWithPermissions): pin change required", .{}); - return fido.ctap.StatusCodes.ctap2_err_pin_policy_violation; - } + if (settings.force_pin_change) { + std.log.err("authenticatorClientPin (getPinUvAuthTokenUsingPinWithPermissions): pin change required", .{}); + return fido.ctap.StatusCodes.ctap2_err_pin_policy_violation; } // Create a new pinUvAuthToken diff --git a/lib/ctap/commands/authenticator/authenticatorCredentialManagement.zig b/lib/ctap/commands/authenticator/authenticatorCredentialManagement.zig index 958b0ef..ecde9e9 100644 --- a/lib/ctap/commands/authenticator/authenticatorCredentialManagement.zig +++ b/lib/ctap/commands/authenticator/authenticatorCredentialManagement.zig @@ -1,6 +1,9 @@ const std = @import("std"); const cbor = @import("zbor"); +const uuid = @import("uuid"); const fido = @import("../../../main.zig"); +const deriveMacKey = fido.ctap.crypto.master_secret.deriveMacKey; +const deriveEncKey = fido.ctap.crypto.master_secret.deriveEncKey; fn validate( cmReq: *const fido.ctap.request.CredentialManagement, @@ -62,20 +65,41 @@ pub fn getKeyInfo( cmResp: *fido.ctap.response.CredentialManagement, auth: *fido.ctap.authenticator.Authenticator, ) ?fido.ctap.StatusCodes { - var entry = auth.callbacks.getEntry(id).?; + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("getKeyInfo: Unable to fetch Settings ({any})", .{err}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("getKeyInfo: Settings MAC validation unsuccessful", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + } - // Get the user id - if (entry.getField("UserId", auth.callbacks.millis())) |user| { - var a = auth.allocator.alloc(u8, user.len) catch { - std.log.err("Out of memory", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - }; - @memcpy(a, user); - cmResp.user = .{ .id = a }; - } else { - std.log.err("authenticatorCredentialManagement (enumerateRPsBegin): unable to fetch UserId", .{}); + const ms = settings.getSecret(auth.secret.enc) catch { + std.log.err("getKeyInfo: unable to decrypt secret", .{}); return fido.ctap.StatusCodes.ctap1_err_other; + }; + + const uid = std.mem.bytesToValue(uuid.Uuid, id[0..16]); + const urn = uuid.urn.serialize(uid); + var entries = auth.callbacks.readCred(.{ .id = urn[0..] }, auth.allocator) catch |err| { + std.log.err("getKeyInfo: unable to fetch credential with id {s} ({any})", .{ + std.fmt.fmtSliceHexUpper(id), + err, + }); + return fido.ctap.StatusCodes.ctap2_err_no_credentials; + }; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); } + var entry = entries[0]; + + cmResp.user = .{ .id = auth.allocator.dupe(u8, entry.user_id) catch { + return fido.ctap.StatusCodes.ctap1_err_other; + } }; // Get the credential id cmResp.credentialID = .{ @@ -83,51 +107,38 @@ pub fn getKeyInfo( .type = .@"public-key", }; - // Get the public key - if (entry.getField("PrivateKey", auth.callbacks.millis())) |pk| { - if (entry.getField("Algorithm", auth.callbacks.millis())) |algo| { - const algorithm = std.mem.bytesToValue(cbor.cose.Algorithm, algo[0..4]); + // Get public key + var alg: ?fido.ctap.crypto.SigAlg = null; + for (auth.algorithms) |_alg| blk: { + if (entry.alg == _alg.alg) { + alg = _alg; + break :blk; + } + } - var alg: ?fido.ctap.crypto.SigAlg = null; - for (auth.algorithms) |_alg| blk: { - if (algorithm == _alg.alg) { - alg = _alg; - break :blk; - } - } + if (alg == null) { + // THis is only relevant if we import keys from other authenticators + // or change the settings. + std.log.err("getKeyInfo: Unsupported algorithm {any}", .{entry.alg}); + return fido.ctap.StatusCodes.ctap1_err_other; + } - if (alg == null) { - std.log.err("Unsupported algorithm", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - } + const enc_key = deriveEncKey(ms); + const raw_key = entry.getPrivateKey(enc_key, auth.allocator) catch { + std.log.err("getKeyInfo: unable to decrypt private key", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + defer auth.allocator.free(raw_key); - if (alg.?.from_priv(pk)) |public_key| { - cmResp.publicKey = public_key; - } else { - std.log.err("Unable to derive public key", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - } - } else { - std.log.err("Unable to fetch Algorithm", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - } + if (alg.?.from_priv(raw_key)) |public_key| { + cmResp.publicKey = public_key; } else { - std.log.err("Unable to fetch PrivateKey", .{}); + std.log.err("Unable to derive public key", .{}); return fido.ctap.StatusCodes.ctap1_err_other; } // Get policy - if (entry.getField("Policy", auth.callbacks.millis())) |pol| { - if (fido.ctap.extensions.CredentialCreationPolicy.fromString(pol)) |p| { - cmResp.credProtect = p; - } else { - std.log.err("Unable to translate Policy", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - } - } else { - std.log.err("Unable to fetch Policy", .{}); - return fido.ctap.StatusCodes.ctap1_err_other; - } + cmResp.credProtect = entry.policy; // TODO: Return large blob key (not yet supported) @@ -139,41 +150,23 @@ pub fn authenticatorCredentialManagement( out: anytype, command: []const u8, ) !fido.ctap.StatusCodes { - const cmReq = try cbor.parse( + const cmReq = cbor.parse( fido.ctap.request.CredentialManagement, try cbor.DataItem.new(command[1..]), .{ .allocator = auth.allocator, }, - ); + ) catch |err| { + std.log.err("authenticatorCredentialManagement: Unable to parse arguments ({any})", .{err}); + return err; + }; defer cmReq.deinit(auth.allocator); var cmResp: fido.ctap.response.CredentialManagement = .{}; defer cmResp.deinit(auth.allocator); - // State for different sub-commands - const S = struct { - pub var rpId: ?struct { - ids: std.ArrayList([]const u8), - time_stamp: i64, - prot: fido.ctap.pinuv.common.PinProtocol, - token: [32]u8, - } = null; - - pub fn deinit(a: std.mem.Allocator) void { - if (rpId) |rpId_state| { - for (rpId_state.ids.items) |id| { - a.free(id); - } - rpId_state.ids.deinit(); - - rpId = null; - } - } - }; - // Invalidate state after 30 seconds or if the pin token has changed - if (S.rpId) |rpId_state| { + if (auth.cred_mngmnt) |rpId_state| { var prot = switch (rpId_state.prot) { .V1 => &auth.token.one.?, .V2 => &auth.token.two.?, @@ -181,42 +174,63 @@ pub fn authenticatorCredentialManagement( const diff: i64 = auth.callbacks.millis() - rpId_state.time_stamp; if (diff >= 30000 or !std.mem.eql(u8, prot.pin_token[0..], rpId_state.token[0..])) { - S.deinit(auth.allocator); + rpId_state.deinit(auth.allocator); + auth.cred_mngmnt = null; } } switch (cmReq.subCommand) { - .getCredsMetadata => { + .getCredsMetadata => blk: { if (validate(&cmReq, auth)) |r| { return r; } - const el: u32 = if (auth.callbacks.getEntries(&.{}, auth.allocator)) |entries| blk: { - auth.allocator.free(entries); - break :blk @intCast(entries.len); - } else blk: { - break :blk 0; + var entries = auth.callbacks.readCred(.{ .all = true }, auth.allocator) catch |err| { + std.log.err("getCredsMetadata: unable to fetch credentials ({any})", .{ + err, + }); + cmResp.existingResidentCredentialsCount = 0; + cmResp.maxPossibleRemainingResidentCredentialsCount = 1; + break :blk; }; - cmResp.existingResidentCredentialsCount = el; - cmResp.maxPossibleRemainingResidentCredentialsCount = if (auth.settings.remainingDiscoverableCredentials) |rdc| @as(u32, @intCast(rdc)) - el else 1; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); + } + + cmResp.existingResidentCredentialsCount = @intCast(entries.len); + cmResp.maxPossibleRemainingResidentCredentialsCount = 1; }, .enumerateRPsBegin => { if (validate(&cmReq, auth)) |r| { return r; } + var entries = auth.callbacks.readCred(.{ .all = true }, auth.allocator) catch |err| { + std.log.err("enumerateRPsBegin: unable to fetch credentials ({any})", .{ + err, + }); + return fido.ctap.StatusCodes.ctap2_err_no_credentials; + }; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); + } + // check if discoverable credentials exist on this authenticator - const entries = if (auth.callbacks.getEntries(&.{}, auth.allocator)) |entries| entries else return fido.ctap.StatusCodes.ctap2_err_no_credentials; - defer auth.allocator.free(entries); if (entries.len == 0) return fido.ctap.StatusCodes.ctap2_err_no_credentials; - if (S.rpId == null) { + if (auth.cred_mngmnt == null) { var prot = switch (cmReq.pinUvAuthProtocol.?) { .V1 => &auth.token.one.?, .V2 => &auth.token.two.?, }; - S.rpId = .{ + auth.cred_mngmnt = .{ .ids = std.ArrayList([]const u8).init(auth.allocator), .time_stamp = auth.callbacks.millis(), .prot = cmReq.pinUvAuthProtocol.?, @@ -225,32 +239,25 @@ pub fn authenticatorCredentialManagement( } for (entries) |entry| { - if (entry.getField("RpId", auth.callbacks.millis())) |rpId| { - var a = try auth.allocator.alloc(u8, rpId.len); - @memcpy(a, rpId); - - var found: bool = false; - for (S.rpId.?.ids.items) |id| { - if (std.mem.eql(u8, id, a)) { - found = true; - } + var found: bool = false; + for (auth.cred_mngmnt.?.ids.items) |id| { + if (std.mem.eql(u8, id, entry.rp_id)) { + found = true; } - - if (!found) try S.rpId.?.ids.append(a); - } else { - std.log.warn("authenticatorCredentialManagement (enumerateRPsBegin): credential with id {s} has no associated rpId", .{std.fmt.fmtSliceHexUpper(entry.id)}); } + + if (!found) try auth.cred_mngmnt.?.ids.append(try auth.allocator.dupe(u8, entry.rp_id)); } - cmResp.totalRPs = @intCast(S.rpId.?.ids.items.len); - const id = S.rpId.?.ids.pop(); + cmResp.totalRPs = @intCast(auth.cred_mngmnt.?.ids.items.len); + const id = auth.cred_mngmnt.?.ids.pop(); var idh: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(id, &idh, .{}); cmResp.rpIDHash = idh; cmResp.rp = fido.common.RelyingParty{ .id = id }; }, .enumerateRPsGetNextRP => { - if (S.rpId) |*rpIds| { + if (auth.cred_mngmnt) |*rpIds| { const id = rpIds.ids.pop(); var idh: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(id, &idh, .{}); @@ -273,17 +280,28 @@ pub fn authenticatorCredentialManagement( const rpIdHash = cmReq.subCommandParams.?.rpIDHash.?; - const entries = if (auth.callbacks.getEntries(&.{}, auth.allocator)) |entries| entries else return fido.ctap.StatusCodes.ctap2_err_no_credentials; - defer auth.allocator.free(entries); + var entries = auth.callbacks.readCred(.{ .all = true }, auth.allocator) catch |err| { + std.log.err("enumerateRPsBegin: unable to fetch credentials ({any})", .{ + err, + }); + return fido.ctap.StatusCodes.ctap2_err_no_credentials; + }; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); + } + if (entries.len == 0) return fido.ctap.StatusCodes.ctap2_err_no_credentials; - if (S.rpId == null) { + if (auth.cred_mngmnt == null) { var prot = switch (cmReq.pinUvAuthProtocol.?) { .V1 => &auth.token.one.?, .V2 => &auth.token.two.?, }; - S.rpId = .{ + auth.cred_mngmnt = .{ .ids = std.ArrayList([]const u8).init(auth.allocator), .time_stamp = auth.callbacks.millis(), .prot = cmReq.pinUvAuthProtocol.?, @@ -291,37 +309,30 @@ pub fn authenticatorCredentialManagement( }; } - var RP_ID: ?[]const u8 = null; for (entries) |entry| { - if (entry.getField("RpId", auth.callbacks.millis())) |rpId| { - var idh: [32]u8 = undefined; - std.crypto.hash.sha2.Sha256.hash(rpId, &idh, .{}); + var idh: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(entry.rp_id, &idh, .{}); - if (!std.mem.eql(u8, idh[0..], rpIdHash[0..])) continue; - RP_ID = rpId; + if (!std.mem.eql(u8, idh[0..], rpIdHash[0..])) continue; - var a = try auth.allocator.alloc(u8, entry.id.len); - @memcpy(a, entry.id); - try S.rpId.?.ids.append(a); - } else { - std.log.warn("authenticatorCredentialManagement (enumerateRPsBegin): credential with id {s} has no associated rpId", .{std.fmt.fmtSliceHexUpper(entry.id)}); - } + const uid = try uuid.urn.deserialize(entry._id[0..]); + try auth.cred_mngmnt.?.ids.append(try auth.allocator.dupe(u8, std.mem.asBytes(&uid))); } - if (S.rpId.?.ids.items.len == 0) { + if (auth.cred_mngmnt.?.ids.items.len == 0) { return fido.ctap.StatusCodes.ctap2_err_no_credentials; } // Get total credentials - cmResp.totalCredentials = @intCast(S.rpId.?.ids.items.len); - const id = S.rpId.?.ids.pop(); + cmResp.totalCredentials = @intCast(auth.cred_mngmnt.?.ids.items.len); + const id = auth.cred_mngmnt.?.ids.pop(); if (getKeyInfo(id, &cmResp, auth)) |err| { return err; } }, .enumerateCredentialsGetNextCredential => { - if (S.rpId) |*rpIds| { + if (auth.cred_mngmnt) |*rpIds| { const id = rpIds.ids.pop(); if (getKeyInfo(id, &cmResp, auth)) |err| { @@ -342,15 +353,33 @@ pub fn authenticatorCredentialManagement( return r; } - auth.callbacks.removeEntry(cmReq.subCommandParams.?.credentialID.?.id) catch |err| { + const uid = std.mem.bytesToValue( + uuid.Uuid, + cmReq.subCommandParams.?.credentialID.?.id[0..16], + ); + const urn = uuid.urn.serialize(uid); + var entries = auth.callbacks.readCred(.{ .id = urn[0..] }, auth.allocator) catch |err| { + std.log.err("getCredsMetadata: unable to fetch credentials with id {s} ({any})", .{ + std.fmt.fmtSliceHexUpper(cmReq.subCommandParams.?.credentialID.?.id), + err, + }); + return fido.ctap.StatusCodes.ctap2_err_no_credentials; + }; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); + } + var entry = entries[0]; + + auth.callbacks.deleteCred(&entry) catch |err| { if (err == error.DoesNotExist) { return fido.ctap.StatusCodes.ctap2_err_no_credentials; } else { return fido.ctap.StatusCodes.ctap1_err_other; } }; - - try auth.callbacks.persist(); }, .updateUserInformation => { if (cmReq.subCommandParams == null or @@ -364,59 +393,80 @@ pub fn authenticatorCredentialManagement( return r; } - var entry = if (auth.callbacks.getEntry(cmReq.subCommandParams.?.credentialID.?.id)) |e| e else { + const uid = std.mem.bytesToValue( + uuid.Uuid, + cmReq.subCommandParams.?.credentialID.?.id[0..16], + ); + const urn = uuid.urn.serialize(uid); + var entries = auth.callbacks.readCred(.{ .id = urn[0..] }, auth.allocator) catch |err| { + std.log.err("getCredsMetadata: unable to fetch credentials with id {s} ({any})", .{ + std.fmt.fmtSliceHexUpper(cmReq.subCommandParams.?.credentialID.?.id), + err, + }); return fido.ctap.StatusCodes.ctap2_err_no_credentials; }; - - if (!std.mem.eql( - u8, - cmReq.subCommandParams.?.credentialID.?.id, - cmReq.subCommandParams.?.user.?.id, - )) { - return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; + defer { + for (entries) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(entries); } + var entry = entries[0]; + + // TODO: ??? + //if (!std.mem.eql( + // u8, + // cmReq.subCommandParams.?.credentialID.?.id, + // cmReq.subCommandParams.?.user.?.id, + //)) { + // return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; + //} - // Replace, add or remove the user name if (cmReq.subCommandParams.?.user.?.name) |name| { - if (entry.getField("UserName", auth.callbacks.millis())) |_| { - entry.updateField("UserName", name, auth.callbacks.millis()) catch { - return fido.ctap.StatusCodes.ctap2_err_key_store_full; - }; - } else { - entry.addField(.{ .key = "UserName", .value = name }, auth.callbacks.millis()) catch { - return fido.ctap.StatusCodes.ctap2_err_key_store_full; - }; + if (entry.user_name) |_name| { + auth.allocator.free(_name); } - } else blk: { - const uname = entry.removeField("UserName", auth.callbacks.millis()) catch { - break :blk; // this is a problem but not such a big one - }; - if (uname) |name| { - entry.allocator.free(name.key); - entry.allocator.free(name.value); + entry.user_name = try auth.allocator.dupe(u8, name); + } else { + if (entry.user_name) |_name| { + auth.allocator.free(_name); } + entry.user_name = null; } - // Replace, add or remove the display name - if (cmReq.subCommandParams.?.user.?.displayName) |displayName| { - if (entry.getField("UserDisplayName", auth.callbacks.millis())) |_| { - entry.updateField("UserDisplayName", displayName, auth.callbacks.millis()) catch { - return fido.ctap.StatusCodes.ctap2_err_key_store_full; - }; - } else { - entry.addField(.{ .key = "UserDisplayName", .value = displayName }, auth.callbacks.millis()) catch { - return fido.ctap.StatusCodes.ctap2_err_key_store_full; - }; + if (cmReq.subCommandParams.?.user.?.displayName) |name| { + if (entry.user_display_name) |_name| { + auth.allocator.free(_name); } - } else blk: { - const dname = entry.removeField("UserDisplayName", auth.callbacks.millis()) catch { - break :blk; // this is a problem but not such a big one - }; - if (dname) |name| { - entry.allocator.free(name.key); - entry.allocator.free(name.value); + entry.user_display_name = try auth.allocator.dupe(u8, name); + } else { + if (entry.user_display_name) |_name| { + auth.allocator.free(_name); } + entry.user_display_name = null; } + + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("getKeyInfo: Unable to fetch Settings ({any})", .{err}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("getKeyInfo: Settings MAC validation unsuccessful", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + } + + const ms = settings.getSecret(auth.secret.enc) catch { + std.log.err("getKeyInfo: unable to decrypt secret", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + + const mac_key = deriveMacKey(ms); + entry.updateMac(&mac_key); + auth.callbacks.updateCred(&entry, auth.allocator) catch |err| { + std.log.err("authenticatorMakeCredential: unable to create credential ({any})", .{err}); + return err; + }; }, } diff --git a/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig b/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig index 432090e..0db77a7 100644 --- a/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig +++ b/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig @@ -2,7 +2,10 @@ const std = @import("std"); const cbor = @import("zbor"); const cks = @import("cks"); const fido = @import("../../../main.zig"); +const uuid = @import("uuid"); const helper = @import("helper.zig"); +const deriveMacKey = fido.ctap.crypto.master_secret.deriveMacKey; +const deriveEncKey = fido.ctap.crypto.master_secret.deriveEncKey; pub fn authenticatorGetAssertion( auth: *fido.ctap.authenticator.Authenticator, @@ -145,60 +148,74 @@ pub fn authenticatorGetAssertion( // 7. Locate credentials // ++++++++++++++++++++++++++++++++++++++++++++++++ - var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { - std.log.err("Unable to fetch Settings", .{}); + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("authenticatorGetAssertion: Unable to fetch Settings ({any})", .{err}); return fido.ctap.StatusCodes.ctap1_err_other; }; - - var _ms = if (settings.getField("Secret", auth.callbacks.millis())) |ms| ms else { - std.log.err("Secret field missing in Settings", .{}); + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("authenticatorGetAssertion: Settings MAC validation unsuccessful", .{}); return fido.ctap.StatusCodes.ctap1_err_other; - }; + } - const ms: fido.ctap.crypto.master_secret.MasterSecret = _ms[0..fido.ctap.crypto.master_secret.MS_LEN].*; + const ms = settings.getSecret(auth.secret.enc) catch |err| { + std.log.err("authenticatorGetAssertion: unable to decrypt secret", .{}); + return err; + }; - var credentials = std.ArrayList(fido.ctap.crypto.Id).init( + var credentials = std.ArrayList(fido.ctap.authenticator.Credential).fromOwnedSlice( auth.allocator, + auth.callbacks.readCred(.{ .rpId = gap.rpId }, auth.allocator) catch { + std.log.err("authenticatorGetAssertion: unable to fetch credentials", .{}); + return fido.ctap.StatusCodes.ctap2_err_no_credentials; + }, ); defer { + for (credentials.items) |item| { + item.deinit(auth.allocator); + } credentials.deinit(); } - if (gap.allowList) |allowList| { - for (allowList) |desc| { - const credId = fido.ctap.crypto.Id.from_raw(desc.id[0..], ms, gap.rpId) catch { + var i: usize = 0; + while (true) { + const l = credentials.items.len; + if (i >= l) break; + + if (gap.allowList) |allowList| { + // Remove all credentials not listed in allow list + var found: bool = false; + for (allowList) |desc| { + const uid = std.mem.bytesToValue(uuid.Uuid, desc.id[0..16]); + const urn = uuid.urn.serialize(uid); + + if (std.mem.eql(u8, urn[0..], credentials.items[i]._id)) { + found = true; + break; + } + } + + if (!found) { + const item = credentials.swapRemove(i); + item.deinit(auth.allocator); + // We don't increment i because we swap the last + // with the current element continue; - }; - try credentials.append(credId); - } - } else { - if (auth.callbacks.getEntries( - &.{.{ .key = "RpId", .value = gap.rpId }}, - auth.allocator, - )) |entries| { - defer auth.allocator.free(entries); - - for (entries) |entry| { - // Each credential is bound to a rpId by a MAC, i.e., if this succeeds we know - // that this credential is bound to the specified rpId - const credId = fido.ctap.crypto.Id.from_raw(entry.id[0..], ms, gap.rpId) catch { - continue; - }; - try credentials.append(credId); } } - } - var i: usize = 0; - while (i < credentials.items.len) : (i += 1) { - const policy = credentials.items[i].getPolicy(); + const policy = credentials.items[i].policy; // if credential protection for a credential is marked as // userVerificationRequired, and the "uv" bit is false in // the response, remove that credential from the applicable // credentials list if (fido.ctap.extensions.CredentialCreationPolicy.userVerificationRequired == policy and !uv_response) { - _ = credentials.swapRemove(i); + const item = credentials.swapRemove(i); + item.deinit(auth.allocator); + // We don't increment i because we swap the last + // with the current element + continue; } // if credential protection for a credential is marked as @@ -207,8 +224,14 @@ pub fn authenticatorGetAssertion( // false in the response, remove that credential from the // applicable credentials list if (fido.ctap.extensions.CredentialCreationPolicy.userVerificationOptionalWithCredentialIDList == policy and gap.allowList == null and !uv_response) { - _ = credentials.swapRemove(i); + const item = credentials.swapRemove(i); + item.deinit(auth.allocator); + // We don't increment i because we swap the last + // with the current element + continue; } + + i += 1; } if (credentials.items.len == 0) { @@ -285,146 +308,63 @@ pub fn authenticatorGetAssertion( // ++++++++++++++++++++++++++++++++++++++++++++++++ // 11. + 12. Finally select credential // ++++++++++++++++++++++++++++++++++++++++++++++++ - var user: ?fido.common.User = null; - var usageCnt: u32 = @as(u32, @intCast(settings.times.usageCount)); - - // TODO: we'll just use the most recently created credential for - // now... but should expand this and adhere to the spec - //var cred = credentials.pop(); - //if (auth.callbacks.getEntry(cred.raw[0..])) |entry| { - // if (credentials.items.len > 1 and auth.callbacks.select_discoverable_credential != null) { - // std.log.info("in", .{}); - // var users = std.ArrayList(fido.common.User).init(auth.allocator); - // defer users.deinit(); - // } else { - // // Seems like this is a discoverable credential, because we - // // just discovered it :) - // usageCnt = @as(u32, @intCast(entry.times.usageCount)); - // entry.times.usageCount += 1; - - // if (uv_response) { - // const user_id = entry.getField("UserId", auth.callbacks.millis()); - // if (user_id) |uid| { - // // User identifiable information (name, DisplayName, icon) - // // inside the publicKeyCredentialUserEntity MUST NOT be returned - // // if user verification is not done by the authenticator - // user = .{ .id = uid, .name = null, .displayName = null }; - // } else { - // std.log.warn("UserId field missing for id {s}. Returning the user id is mandatory for resident keys so expect errors.", .{std.fmt.fmtSliceHexUpper(cred.raw[0..])}); - // } - // } - - // if (credentials.items.len >= 1) { - // // Copy the remaining credential Ids for later use by authenticatorGetNextAssertion - // auth.credential_list = .{ - // .list = try auth.allocator.dupe(fido.ctap.crypto.Id, credentials.items), - // .time_stamp = auth.callbacks.millis(), - // }; - // } - // } - //} else { - // settings.times.usageCount += 1; - //} - - var cred = if (gap.allowList == null) blk: { - if (credentials.items.len > 1 and auth.callbacks.select_discoverable_credential != null and - (up or uv)) - { - var users = std.ArrayList(fido.common.User).init(auth.allocator); - defer users.deinit(); - - // TODO: allow selection of credential - - break :blk credentials.pop(); - } else { - var _cred = credentials.pop(); - // There should always be a credential, but we can't be sure - var entry = if (auth.callbacks.getEntry(_cred.raw[0..])) |entry| entry else return fido.ctap.StatusCodes.ctap2_err_no_credentials; - // Seems like this is a discoverable credential, because we - // just discovered it :) - usageCnt = @as(u32, @intCast(entry.times.usageCount)); - entry.times.usageCount += 1; - - if (uv_response) { - const user_id = entry.getField("UserId", auth.callbacks.millis()); - if (user_id) |uid| { - // User identifiable information (name, DisplayName, icon) - // inside the publicKeyCredentialUserEntity MUST NOT be returned - // if user verification is not done by the authenticator - user = .{ .id = uid, .name = null, .displayName = null }; - } else { - std.log.warn( - "UserId field missing for id {s}", - .{std.fmt.fmtSliceHexUpper(_cred.raw[0..])}, - ); - } - } + var cred = if (gap.allowList == null and credentials.items.len > 1 and auth.callbacks.select_discoverable_credential != null and + (up or uv)) + blk: { + var users = std.ArrayList(fido.common.User).init(auth.allocator); + defer users.deinit(); - if (credentials.items.len >= 1) { - // Copy the remaining credential Ids for later use by authenticatorGetNextAssertion - auth.credential_list = .{ - .list = try auth.allocator.dupe(fido.ctap.crypto.Id, credentials.items), - .time_stamp = auth.callbacks.millis(), - }; - } + // TODO: allow selection of credential - break :blk _cred; - } + break :blk credentials.pop(); } else blk: { - var _cred = credentials.pop(); - - if (auth.callbacks.getEntry(_cred.raw[0..])) |entry| { - // This is a discoverable credential a.k.a. PassKey but it was used as - // second factor, i.e. the RP used a allowList. We have to return the - // user as this is mandatory of discoverable credentials. - usageCnt = @as(u32, @intCast(entry.times.usageCount)); - entry.times.usageCount += 1; - - if (uv_response) { - const user_id = entry.getField("UserId", auth.callbacks.millis()); - if (user_id) |uid| { - // User identifiable information (name, DisplayName, icon) - // inside the publicKeyCredentialUserEntity MUST NOT be returned - // if user verification is not done by the authenticator - user = .{ .id = uid, .name = null, .displayName = null }; - } else { - std.log.warn( - "UserId field missing for id {s}", - .{std.fmt.fmtSliceHexUpper(_cred.raw[0..])}, - ); - } - } - } else { - settings.times.usageCount += 1; - } - - break :blk _cred; + break :blk credentials.pop(); }; + defer cred.deinit(auth.allocator); + + // Seems like this is a discoverable credential, because we + // just discovered it :) + var usageCnt = cred.sign_count; + cred.sign_count += 1; + + var user = if (uv_response) blk: { + // User identifiable information (name, DisplayName, icon) + // inside the publicKeyCredentialUserEntity MUST NOT be returned + // if user verification is not done by the authenticator + break :blk fido.common.User{ + .id = cred.user_id, + .name = cred.user_name, + .displayName = cred.user_display_name, + }; + } else blk: { + break :blk null; + }; + + if (credentials.items.len >= 1) { + // Copy the remaining credential Ids for later use by authenticatorGetNextAssertion + auth.credential_list = .{ + .list = try auth.allocator.dupe(fido.ctap.authenticator.Credential, credentials.items), + .time_stamp = auth.callbacks.millis(), + }; + } // select algorithm based on credential - const algorithm = cred.getAlg(); var alg: ?fido.ctap.crypto.SigAlg = null; for (auth.algorithms) |_alg| blk: { - if (algorithm == _alg.alg) { + if (cred.alg == _alg.alg) { alg = _alg; break :blk; } } if (alg == null) { - std.log.err("Unknown algorithm for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&cred.raw)}); + std.log.err("Unknown algorithm for credential with id: {s}", .{std.fmt.fmtSliceHexLower(cred._id)}); return fido.ctap.StatusCodes.ctap1_err_other; } - const seed = cred.deriveSeed(ms); - const key_pair = if (alg.?.create_det( - &seed, - auth.allocator, - )) |kp| kp else return fido.ctap.StatusCodes.ctap1_err_other; - defer { - auth.allocator.free(key_pair.cose_public_key); - auth.allocator.free(key_pair.raw_private_key); - } + const enc_key = deriveEncKey(ms); + const raw_key = try cred.getPrivateKey(enc_key, auth.allocator); + defer auth.allocator.free(raw_key); // ++++++++++++++++++++++++++++++++++++++++++++++++ // 13. Sign data @@ -439,7 +379,7 @@ pub fn authenticatorGetAssertion( .at = 0, .ed = 0, }, - .signCount = usageCnt, + .signCount = @intCast(usageCnt), .extensions = extensions, }; std.crypto.hash.sha2.Sha256.hash( // calculate rpId hash @@ -462,26 +402,32 @@ pub fn authenticatorGetAssertion( // v // ASSERTION SIGNATURE const sig = if (alg.?.sign( - key_pair.raw_private_key, + raw_key, &.{ authData.items, &gap.clientDataHash }, auth.allocator, )) |signature| signature else { - std.log.err("signature creation failed for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&cred.raw)}); + std.log.err("signature creation failed for credential with id: {s}", .{std.fmt.fmtSliceHexLower(cred._id)}); return fido.ctap.StatusCodes.ctap1_err_other; }; defer auth.allocator.free(sig); + const uid = try uuid.urn.deserialize(cred._id[0..]); const gar = fido.ctap.response.GetAssertion{ .credential = .{ .type = .@"public-key", - .id = &cred.raw, + .id = std.mem.asBytes(&uid), }, .authData = authData.items, .signature = sig, .user = user, }; - try auth.callbacks.persist(); + const mac_key = deriveMacKey(ms); + cred.updateMac(&mac_key); + auth.callbacks.updateCred(&cred, auth.allocator) catch |err| { + std.log.err("authenticatorGetAssertion: unable to update credential ({any})", .{err}); + return err; + }; try cbor.stringify(gar, .{ .allocator = auth.allocator }, out); diff --git a/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig b/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig index 3650189..6317f19 100644 --- a/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig +++ b/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig @@ -2,7 +2,10 @@ const std = @import("std"); const cbor = @import("zbor"); const cks = @import("cks"); const fido = @import("../../../main.zig"); +const uuid = @import("uuid"); const helper = @import("helper.zig"); +const deriveMacKey = fido.ctap.crypto.master_secret.deriveMacKey; +const deriveEncKey = fido.ctap.crypto.master_secret.deriveEncKey; pub fn authenticatorGetNextAssertion( auth: *fido.ctap.authenticator.Authenticator, @@ -15,84 +18,66 @@ pub fn authenticatorGetNextAssertion( if (auth.credential_list.?.credentialCounter >= auth.credential_list.?.list.len or (auth.callbacks.millis() - auth.credential_list.?.time_stamp) >= 30000) { - auth.allocator.free(auth.credential_list.?.list); + auth.credential_list.?.deinit(auth.allocator); auth.credential_list = null; return fido.ctap.StatusCodes.ctap2_err_not_allowed; } - // Fetch authenticator settings to get master secret - var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { - std.log.err("Unable to fetch Settings", .{}); + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("authenticatorGetNextAssertion: Unable to fetch Settings ({any})", .{err}); return fido.ctap.StatusCodes.ctap1_err_other; }; - - var _ms = if (settings.getField("Secret", auth.callbacks.millis())) |ms| ms else { - std.log.err("Secret field missing in Settings", .{}); + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("authenticatorGetNextAssertion: Settings MAC validation unsuccessful", .{}); return fido.ctap.StatusCodes.ctap1_err_other; - }; + } - const ms: fido.ctap.crypto.master_secret.MasterSecret = _ms[0..fido.ctap.crypto.master_secret.MS_LEN].*; + const ms = try settings.getSecret(auth.secret.enc); // Fetch next credential id - const id = auth.credential_list.?.list[auth.credential_list.?.credentialCounter]; + var cred = auth.credential_list.?.list[auth.credential_list.?.credentialCounter]; auth.credential_list.?.credentialCounter += 1; // Fetch the credential based on credential id and update the return data var user: ?fido.common.User = null; - if (auth.callbacks.getEntry(id.raw[0..])) |entry| { - // Seems like this is a discoverable credential, because we - // just discovered it :) - auth.credential_list.?.authData.signCount = @as(u32, @intCast(entry.times.usageCount)); - entry.times.usageCount += 1; - - if (auth.credential_list.?.authData.flags.uv == 1) { - // publicKeyCredentialUserEntity MUST NOT be returned if user verification - // was not done by the authenticator in the original authenticatorGetAssertion call - const user_id = entry.getField("UserId", auth.callbacks.millis()); - if (user_id) |uid| { - // User identifiable information (name, DisplayName, icon) - // inside the publicKeyCredentialUserEntity MUST NOT be returned - // if user verification is not done by the authenticator - user = .{ .id = uid, .name = null, .displayName = null }; - } else { - std.log.warn( - "UserId field missing for id: {s}", - .{std.fmt.fmtSliceHexUpper(id.raw[0..])}, - ); - } - } - } else { - std.log.warn( - "Unable to load credential with id: {s}", - .{std.fmt.fmtSliceHexUpper(id.raw[0..])}, - ); - return fido.ctap.StatusCodes.ctap1_err_other; + + // Seems like this is a discoverable credential, because we + // just discovered it :) + auth.credential_list.?.authData.signCount = @as(u32, @intCast(cred.sign_count)); + cred.sign_count += 1; + + if (auth.credential_list.?.authData.flags.uv == 1) { + // publicKeyCredentialUserEntity MUST NOT be returned if user verification + // was not done by the authenticator in the original authenticatorGetAssertion call + + // User identifiable information (name, DisplayName, icon) + // inside the publicKeyCredentialUserEntity MUST NOT be returned + // if user verification is not done by the authenticator + user = .{ + .id = cred.user_id, + .name = cred.user_name, + .displayName = cred.user_display_name, + }; } // select algorithm based on credential - const algorithm = id.getAlg(); var alg: ?fido.ctap.crypto.SigAlg = null; for (auth.algorithms) |_alg| blk: { - if (algorithm == _alg.alg) { + if (cred.alg == _alg.alg) { alg = _alg; break :blk; } } if (alg == null) { - std.log.err("Unknown algorithm for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&id.raw)}); + std.log.err("Unknown algorithm for credential with id: {s}", .{std.fmt.fmtSliceHexLower(cred._id)}); return fido.ctap.StatusCodes.ctap1_err_other; } - const seed = id.deriveSeed(ms); - const key_pair = if (alg.?.create_det( - &seed, - auth.allocator, - )) |kp| kp else return fido.ctap.StatusCodes.ctap1_err_other; - defer { - auth.allocator.free(key_pair.cose_public_key); - auth.allocator.free(key_pair.raw_private_key); - } + const enc_key = deriveEncKey(ms); + const raw_key = try cred.getPrivateKey(enc_key, auth.allocator); + defer auth.allocator.free(raw_key); // Sign the data var authData = std.ArrayList(u8).init(auth.allocator); @@ -100,26 +85,32 @@ pub fn authenticatorGetNextAssertion( try auth.credential_list.?.authData.encode(authData.writer()); const sig = if (alg.?.sign( - key_pair.raw_private_key, + raw_key, &.{ authData.items, &auth.credential_list.?.clientDataHash }, auth.allocator, )) |signature| signature else { - std.log.err("signature creation failed for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&id.raw)}); + std.log.err("signature creation failed for credential with id: {s}", .{std.fmt.fmtSliceHexLower(cred._id)}); return fido.ctap.StatusCodes.ctap1_err_other; }; defer auth.allocator.free(sig); + const uid = try uuid.urn.deserialize(cred._id[0..]); const gar = fido.ctap.response.GetAssertion{ .credential = .{ .type = .@"public-key", - .id = &id.raw, + .id = std.mem.asBytes(&uid), }, .authData = authData.items, .signature = sig, .user = user, }; - try auth.callbacks.persist(); + const mac_key = deriveMacKey(ms); + cred.updateMac(&mac_key); + auth.callbacks.updateCred(&cred, auth.allocator) catch |err| { + std.log.err("authenticatorGetAssertion: unable to update credential ({any})", .{err}); + return err; + }; try cbor.stringify(gar, .{ .allocator = auth.allocator }, out); diff --git a/lib/ctap/commands/authenticator/authenticatorMakeCredential.zig b/lib/ctap/commands/authenticator/authenticatorMakeCredential.zig index 6dfa0c6..1206f99 100644 --- a/lib/ctap/commands/authenticator/authenticatorMakeCredential.zig +++ b/lib/ctap/commands/authenticator/authenticatorMakeCredential.zig @@ -1,8 +1,11 @@ const std = @import("std"); const cbor = @import("zbor"); const cks = @import("cks"); +const uuid = @import("uuid"); const fido = @import("../../../main.zig"); const helper = @import("helper.zig"); +const deriveMacKey = fido.ctap.crypto.master_secret.deriveMacKey; +const deriveEncKey = fido.ctap.crypto.master_secret.deriveEncKey; pub fn authenticatorMakeCredential( auth: *fido.ctap.authenticator.Authenticator, @@ -201,25 +204,44 @@ pub fn authenticatorMakeCredential( // 12. Check exclude list // ++++++++++++++++++++++++++++++++++++++++++++++++ - var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { - std.log.err("Unable to fetch Settings", .{}); + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("authenticatorMakeCredential: Unable to fetch Settings ({any})", .{err}); return fido.ctap.StatusCodes.ctap1_err_other; }; - - var _ms = if (settings.getField("Secret", auth.callbacks.millis())) |ms| ms else { - std.log.err("Secret field missing in Settings", .{}); + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("authenticatorMakeCredential: Settings MAC validation unsuccessful", .{}); return fido.ctap.StatusCodes.ctap1_err_other; - }; + } - const ms: fido.ctap.crypto.master_secret.MasterSecret = _ms[0..fido.ctap.crypto.master_secret.MS_LEN].*; + const ms = try settings.getSecret(auth.secret.enc); + // The authenticator returns an error if the authenticator already contains one of the credentials + // enumerated in this array. This allows RPs to limit the creation of multiple credentials for the + // same account on a single authenticator. if (mcp.excludeList) |ecllist| { for (ecllist) |ecl| { - const credId = fido.ctap.crypto.Id.from_raw(ecl.id[0..], ms, mcp.rp.id) catch { + const uid = std.mem.bytesToValue(uuid.Uuid, ecl.id[0..16]); + const urn = uuid.urn.serialize(uid); + + var cred = auth.callbacks.readCred(.{ .id = urn[0..] }, auth.allocator) catch { + // TODO: return error for all errors except DoesNotExist + + // If we cant find the credential, it doesn't exist continue; }; + if (cred.len == 0) continue; + defer { + cred[0].deinit(auth.allocator); + auth.allocator.free(cred); + } + const mac_key = deriveMacKey(ms); + if (!cred[0].verifyMac(&mac_key)) { + // MAC validation failed + continue; + } - const cred_policy = credId.getPolicy(); + const cred_policy = cred[0].policy; if (fido.ctap.extensions.CredentialCreationPolicy.userVerificationRequired != cred_policy) { var userPresentFlagValue = false; @@ -325,53 +347,35 @@ pub fn authenticatorMakeCredential( .credProtect = policy, }; - const id = fido.ctap.crypto.Id.new(alg.?.alg, policy, ms, mcp.rp.id, auth.callbacks.rand); + // Create a new universally unique identifier as ID + const id = uuid.v4.new2(auth.callbacks.rand); // Create Entry if rk required - var entry: ?cks.Entry = if (rk) try auth.callbacks.createEntry(id.raw[0..]) else null; - errdefer { - if (rk) { - entry.?.deinit(); - } - } - - if (rk) { - try entry.?.addField( - .{ .key = "Policy", .value = policy.toString() }, - auth.callbacks.millis(), - ); - } - - if (rk and auth.extensionSupported(.@"hmac-secret")) { + var entry = try fido.ctap.authenticator.Credential.allocInit( + id, + &mcp.user, + mcp.rp.id, + alg.?.alg, + policy, + auth.allocator, // The authenticator generates two random 32-byte values (called CredRandomWithUV // and CredRandomWithoutUV) and associates them with the credential. - var random_mem: [32]u8 = undefined; - auth.callbacks.rand.bytes(random_mem[0..]); - try entry.?.addField( - .{ .key = "CredRandomWithUV", .value = random_mem[0..] }, - auth.callbacks.millis(), - ); - auth.callbacks.rand.bytes(random_mem[0..]); - try entry.?.addField( - .{ .key = "CredRandomWithoutUV", .value = random_mem[0..] }, - auth.callbacks.millis(), - ); - } + auth.callbacks.rand, + ); + defer entry.deinit(auth.allocator); if (mcp.extensions) |ext| { - if (rk) { - // Prepare hmac-secret - if (ext.@"hmac-secret") |hsec| { - switch (hsec) { - .create => |flag| { - // The creation of the two random values will always succeed, - // so we'll always return true. - if (flag) { - extensions.@"hmac-secret" = .{ .create = true }; - } - }, - else => {}, - } + // Prepare hmac-secret + if (ext.@"hmac-secret") |hsec| { + switch (hsec) { + .create => |flag| { + // The creation of the two random values will always succeed, + // so we'll always return true. + if (flag) { + extensions.@"hmac-secret" = .{ .create = true }; + } + }, + else => {}, } } } @@ -379,10 +383,8 @@ pub fn authenticatorMakeCredential( // ++++++++++++++++++++++++++++++++++++++++++++++++ // 16. Create a new credential // ++++++++++++++++++++++++++++++++++++++++++++++++ - const seed = id.deriveSeed(ms); - - const key_pair = if (alg.?.create_det( - &seed, + const key_pair = if (alg.?.create( + auth.callbacks.rand, auth.allocator, )) |kp| kp else return fido.ctap.StatusCodes.ctap1_err_other; defer { @@ -390,90 +392,71 @@ pub fn authenticatorMakeCredential( auth.allocator.free(key_pair.raw_private_key); } - const usageCnt = if (rk) blk: { - try entry.?.addField( - .{ .key = "RpId", .value = mcp.rp.id }, - auth.callbacks.millis(), - ); - - try entry.?.addField( - .{ .key = "UserId", .value = mcp.user.id }, - auth.callbacks.millis(), - ); - - if (mcp.user.name) |name| { - try entry.?.addField( - .{ .key = "UserName", .value = name }, - auth.callbacks.millis(), - ); - } - - if (mcp.user.displayName) |name| { - try entry.?.addField( - .{ .key = "UserDisplayName", .value = name }, - auth.callbacks.millis(), - ); - } - - const cnt = entry.?.times.usageCount; - entry.?.times.usageCount = 1; // This includes the first signature possibly made below - - try entry.?.addField( - .{ .key = "PrivateKey", .value = key_pair.raw_private_key }, - auth.callbacks.millis(), - ); + const enc_key = deriveEncKey(ms); + try entry.setPrivateKey( + key_pair.raw_private_key, + enc_key, + auth.callbacks.rand, + auth.allocator, + ); - try entry.?.addField( - .{ .key = "Algorithm", .value = &alg.?.alg.to_raw() }, - auth.callbacks.millis(), - ); - break :blk cnt; - } else blk: { - const cnt = settings.times.usageCount; - // This is the global usage counter - settings.times.usageCount += 1; - break :blk cnt; - }; + const usageCnt = entry.sign_count; + entry.sign_count += 1; // ++++++++++++++++++++++++++++++++++++++++++++++++ // 17. + 18. Store credential // ++++++++++++++++++++++++++++++++++++++++++++++++ - if (rk) { - // If a credential for the same rp.id and account ID already exists - // on the authenticator, overwrite that credential. - if (auth.callbacks.getEntries( - &.{ - .{ .key = "RpId", .value = mcp.rp.id }, - .{ .key = "UserId", .value = mcp.user.id }, - }, - auth.allocator, - )) |entries| { - defer auth.allocator.free(entries); - - if (entries.len > 1) { - std.log.warn("Found two discoverable credentials with the same rpId and uId. This shouldn't be!", .{}); + var credentials: ?[]fido.ctap.authenticator.Credential = auth.callbacks.readCred(.{ .rpId = mcp.rp.id }, auth.allocator) catch |err| blk: { + if (err == error.DoesNotExist) { + break :blk null; + } else { + std.log.err("authenticatorMakeCredential: unable to fetch credentials", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; } + }; + defer { + if (credentials != null) { + for (credentials.?) |item| { + item.deinit(auth.allocator); + } + auth.allocator.free(credentials.?); + } + } - std.log.info( - "Overwriting credential with id: {s}", - .{std.fmt.fmtSliceHexUpper(entries[0].id)}, - ); - // Update the old entry - try entries[0].update(&entry.?, auth.callbacks.millis()); - // We don't need the new entry anymore - entry.?.deinit(); - } else { - auth.callbacks.addEntry(entry.?) catch { - return fido.ctap.StatusCodes.ctap2_err_key_store_full; - }; + if (credentials) |creds| { + for (creds) |item| { + if (std.mem.eql(u8, item.user_id, entry.user_id)) { + // If a credential for the same rp.id and account ID already exists + // on the authenticator, overwrite that credential. + std.log.warn("makeCredential: rk with the same user and rp id already exist", .{}); + std.log.info("makeCredential: overwriting existing credentials with id {s}", .{ + item._id, + }); + auth.allocator.free(entry._id); + entry._id = try auth.allocator.dupe(u8, item._id); + if (item._rev) |rev| { + entry._rev = try auth.allocator.dupe(u8, rev); + } + } + } } + + entry.discoverable = true; } - try auth.callbacks.persist(); + + std.debug.print("{any}\n", .{entry}); + const mac_key = deriveMacKey(ms); + entry.updateMac(&mac_key); + auth.callbacks.updateCred(&entry, auth.allocator) catch |err| { + std.log.err("authenticatorMakeCredential: unable to create credential ({any})", .{err}); + return err; + }; // ++++++++++++++++++++++++++++++++++++++++++++++++ // 19. Create attestation statement // ++++++++++++++++++++++++++++++++++++++++++++++++ + const uid = try uuid.urn.deserialize(entry._id[0..]); var auth_data = fido.common.AuthenticatorData{ .rpIdHash = undefined, .flags = .{ @@ -487,8 +470,8 @@ pub fn authenticatorMakeCredential( .signCount = @as(u32, @intCast(usageCnt)), .attestedCredentialData = .{ .aaguid = auth.settings.aaguid, - .credential_length = @as(u16, @intCast(id.raw[0..].len)), - .credential_id = id.raw[0..], + .credential_length = 16, + .credential_id = std.mem.asBytes(&uid), .credential_public_key = key_pair.cose_public_key, }, .extensions = extensions, @@ -525,6 +508,7 @@ pub fn authenticatorMakeCredential( }; }, }; + defer stmt.deinit(auth.allocator); const ao = fido.ctap.response.MakeCredential{ .fmt = fido.common.AttestationStatementFormatIdentifiers.@"packed", diff --git a/lib/ctap/commands/authenticator/get_info.zig b/lib/ctap/commands/authenticator/get_info.zig index 668aca1..42949cf 100644 --- a/lib/ctap/commands/authenticator/get_info.zig +++ b/lib/ctap/commands/authenticator/get_info.zig @@ -10,13 +10,18 @@ pub fn authenticatorGetInfo( ) !fido.ctap.StatusCodes { // Fetch dynamic settings, these will override the static settings set // during instantiation. - var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { - std.log.err("Unable to fetch Settings", .{}); + var settings = auth.callbacks.readSettings(auth.allocator) catch |err| { + std.log.err("authenticatorGetAssertion: Unable to fetch Settings ({any})", .{err}); return fido.ctap.StatusCodes.ctap1_err_other; }; + defer settings.deinit(auth.allocator); + if (!settings.verifyMac(&auth.secret.mac)) { + std.log.err("authenticatorGetAssertion: Settings MAC validation unsuccessful", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + } // Check if we have set a pin - if (settings.getField("Pin", auth.callbacks.millis())) |_| { + if (settings.pin != null) { // null means no client pin support => we wont change that! if (auth.settings.options.?.clientPin != null) { auth.settings.options.?.clientPin = true; @@ -28,29 +33,18 @@ pub fn authenticatorGetInfo( } } - if (settings.getField("ForcePinChange", auth.callbacks.millis())) |fpc| { - if (std.mem.eql(u8, fpc, "True")) { - auth.settings.forcePINChange = true; - } else { - auth.settings.forcePINChange = false; - } + if (settings.force_pin_change) { + auth.settings.forcePINChange = true; } else { - auth.settings.forcePINChange = null; + auth.settings.forcePINChange = false; } - if (settings.getField("MinPinLength", auth.callbacks.millis())) |mpl| { - std.debug.print("minPinLength: {d}\n", .{mpl[0]}); - auth.settings.minPINLength = mpl[0]; - } + auth.settings.minPINLength = settings.min_pin_length; - if (settings.getField("AlwaysUv", auth.callbacks.millis())) |auv| { - if (std.mem.eql(u8, auv, "True")) { - auth.settings.options.?.alwaysUv = true; - } else { - auth.settings.options.?.alwaysUv = false; - } + if (settings.always_uv) { + auth.settings.options.?.alwaysUv = true; } else { - auth.settings.options.?.alwaysUv = null; + auth.settings.options.?.alwaysUv = false; } try cbor.stringify(auth.settings, .{}, out); diff --git a/lib/ctap/crypto/master_secret.zig b/lib/ctap/crypto/master_secret.zig index b40a987..0ec718e 100644 --- a/lib/ctap/crypto/master_secret.zig +++ b/lib/ctap/crypto/master_secret.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Hmac = std.crypto.auth.hmac.sha2.HmacSha256; const Hkdf = std.crypto.kdf.hkdf.HkdfSha256; +const Aes256Ocb = std.crypto.aead.aes_ocb.Aes256Ocb; pub const MS_LEN = Hkdf.prk_length; /// Stored by the authenticator and used to derive all other secrets @@ -13,3 +15,16 @@ pub fn createMasterSecret(rand: std.rand.Random) MasterSecret { rand.bytes(salt[0..]); return Hkdf.extract(&salt, &ikm); } + +/// Derive a deterministic sub-key for message authentication codes. +pub fn deriveMacKey(ms: MasterSecret) [Hmac.mac_length]u8 { + var mac_key: [Hmac.mac_length]u8 = undefined; + Hkdf.expand(mac_key[0..], "MAC", ms); + return mac_key; +} + +pub fn deriveEncKey(ms: MasterSecret) [Aes256Ocb.key_length]u8 { + var enc_key: [Aes256Ocb.key_length]u8 = undefined; + Hkdf.expand(enc_key[0..], "ENC", ms); + return enc_key; +} diff --git a/lib/ctap/response/GetAssertion.zig b/lib/ctap/response/GetAssertion.zig index bcbf1bf..95baf96 100644 --- a/lib/ctap/response/GetAssertion.zig +++ b/lib/ctap/response/GetAssertion.zig @@ -55,6 +55,7 @@ pub fn cborStringify(self: *const @This(), options: cbor.StringifyOptions, out: .{ .name = "numberOfCredentials", .alias = "5", .options = .{} }, .{ .name = "userSelected", .alias = "6", .options = .{} }, .{ .name = "largeBlobKey", .alias = "7", .options = .{} }, + .{ .name = "id", .options = .{ .slice_as_text = false } }, }, .from_cborStringify = true, }, diff --git a/lib/main.zig b/lib/main.zig index e97bff8..76f20c7 100644 --- a/lib/main.zig +++ b/lib/main.zig @@ -163,6 +163,10 @@ pub const ctap = struct { pub const Authenticator = @import("ctap/auth/Authenticator.zig"); /// Authenticator callbacks the user must provide pub const Callbacks = @import("ctap/auth/Callbacks.zig"); + /// Representation of a discoverable credential + pub const Credential = @import("ctap/auth/Credential.zig"); + /// Authenticator related data + pub const Meta = @import("ctap/auth/Meta.zig"); }; /// CTAP commands @@ -228,6 +232,8 @@ pub const ctap = struct { _ = authenticator.Options; _ = authenticator.Callbacks; _ = authenticator.Authenticator; + _ = authenticator.Credential; + _ = authenticator.Meta; _ = transports.ctaphid.Cmd; _ = transports.ctaphid.message; _ = transports.ctaphid.authenticator; diff --git a/platform-auth/README.md b/platform-auth/README.md new file mode 100644 index 0000000..be4ec5a --- /dev/null +++ b/platform-auth/README.md @@ -0,0 +1,21 @@ +# Platform authenticator reference implementation for Linux + +This is a reference implementation I use for testing. It's a little bit hacky but it works. + +This impl depends on the following: + +* `/dev/uhid` - To create a virtual usb hid interface for the client +* `libnotify` - User interaction (user presence checks) +* [CouchDB](https://docs.couchdb.org/en/stable/install/index.html) - For storing credentials. Please make sure you have CochDB installed + +This impl was tested on Ubuntu `22.04`. + +## Getting started + +1. Install CouchDB on your system (remember your username and password) +2. Download the [Zig 0.11.0 compiler](https://ziglang.org/download/) +3. Run `git clone https://github.com/r4gus/fido2 && cd fido2` +4. Compile the project with `zig build` +5. Run the program with `./zig-out/bin/passkee ` + +> The first argument is the password used to encrypt all other data. diff --git a/platform-auth/main.zig b/platform-auth/main.zig index d9ae529..b138597 100644 --- a/platform-auth/main.zig +++ b/platform-auth/main.zig @@ -2,6 +2,8 @@ const std = @import("std"); const cks = @import("cks"); const fido = @import("fido"); const hid = @import("hid.zig"); +const profiling_allocator = @import("profiling_allocator"); +const snorlax = @import("snorlax"); const notify = @cImport({ @cInclude("libnotify/notify.h"); @@ -11,7 +13,9 @@ const uhid = @cImport( @cInclude("linux/uhid.h"), ); -pub var loop: ?*notify.GMainLoop = null; +const signal = @cImport( + @cInclude("signal.h"), +); var store: ?cks.CKS = null; @@ -23,14 +27,21 @@ var uv: ?bool = null; var notification: [*c]notify.NotifyNotification = undefined; -//const la = std.heap.LoggingAllocator(.debug, .debug); -//var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -//var lagpa = la.init(gpa.allocator()); -//var allocator = lagpa.allocator(); +var quit: bool = false; + +var couch_user: ?[]const u8 = null; +var couch_pw: ?[]const u8 = null; var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); +fn intHandler(dummy: c_int) callconv(.C) void { + _ = dummy; + std.log.info("shutting down keepass", .{}); + quit = true; + notify.g_main_context_wakeup(null); +} + fn accept_callback( n: [*c]notify.NotifyNotification, action: [*c]u8, @@ -62,8 +73,7 @@ fn packet_callback(user_data: notify.gpointer) callconv(.C) notify.gboolean { var event = std.mem.zeroes(uhid.uhid_event); const l = device.read(std.mem.asBytes(&event)) catch { - std.log.err("unable to read from device", .{}); - return 0; + return 1; }; _ = l; @@ -113,8 +123,21 @@ fn packet_callback(user_data: notify.gpointer) callconv(.C) notify.gboolean { return 1; } +inline fn slice(in: [*:0]u8) []const u8 { + var i: usize = 0; + while (in[i] != 0) : (i += 1) {} + return in[0..i]; +} + pub fn main() !void { - loop = notify.g_main_loop_new(null, 0); + if (std.os.argv.len < 4) { + try std.io.getStdErr().writeAll("usage: ./passkee password CouchDB_USER CouchDB_PW\n"); + return; + } + + const password = slice(std.os.argv[1]); + couch_user = slice(std.os.argv[2]); + couch_pw = slice(std.os.argv[3]); const interval_ms: notify.guint = 10; var source = notify.g_timeout_source_new(interval_ms); @@ -124,22 +147,23 @@ pub fn main() !void { _ = notify.notify_init("Hello world!"); defer notify.notify_uninit(); - // ------------------- Load db ---------------------------- - const pw = password("password"); + var context: ?*notify.GMainContext = notify.g_main_context_default(); - store = load_key_store(allocator, pw.?) catch { - std.log.err("error: unable to open key store\n", .{}); - return; - }; - // -------------------------------------------------------- + // Here we register the interrupt handler for (ctrl + c). This will + // allow us to break out of the main loop by setting quit to false. + _ = signal.signal(signal.SIGINT, intHandler); // ------------------- Setup USB HID ---------------------- const path = "/dev/uhid"; - device = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch { + device = std.fs.openFileAbsolute(path, .{ + .mode = .read_write, + }) catch { std.log.err("Can't open uhid-cdev {s}\n", .{path}); return; }; defer device.close(); + const flags = try std.os.fcntl(device.handle, 3, 0); + _ = try std.os.fcntl(device.handle, 4, flags | 2048); try create(device); defer destroy(device) catch unreachable; @@ -174,12 +198,11 @@ pub fn main() !void { .rand = std.crypto.random, .millis = std.time.milliTimestamp, .up = up, - .createEntry = createEntry, - .getEntry = getEntry, - .getEntries = getEntries, - .addEntry = addEntry, - .removeEntry = removeEntry, - .persist = persist, + .readSettings = readSettings, + .updateSettings = updateSettings, + .readCred = readCred, + .updateCred = updateCred, + .deleteCred = deleteCred, .reset = reset, }, .algorithms = &.{ @@ -192,129 +215,16 @@ pub fn main() !void { .allocator = allocator, }; - if (authenticator.token.one) |*one| { - one.initialize(); - } - if (authenticator.token.two) |*two| { - two.initialize(); - } - - try authenticator.init(); + try authenticator.init(password); defer authenticator.deinit(); // -------------------------------------------------------- - notify.g_main_loop_run(loop); -} - -// +++++++++++++++++++++++++++++++++++++++++++++ -// Store -// +++++++++++++++++++++++++++++++++++++++++++++ - -const CONFIG_DIR_NAME = ".passkee"; - -fn load_key_store(a: std.mem.Allocator, pw: []const u8) !cks.CKS { - // Get path to the users home folder - const home = try getHome(a); - defer a.free(home); - - // Open the passkee config folder - var config_path = try a.alloc(u8, home.len + CONFIG_DIR_NAME.len + 1); - @memcpy(config_path[0..home.len], home); - config_path[home.len] = '/'; - @memcpy(config_path[home.len + 1 ..], CONFIG_DIR_NAME); - defer a.free(config_path); - - var config_dir = try openConfigFolder(config_path); - - // Try to load database file - createFile(config_dir, "secrets.cks", pw, a) catch {}; // always try to create db - const data = try loadFile(config_dir, "secrets.cks", a); - - return try cks.CKS.open( - data, - pw, - a, - std.crypto.random, - std.time.milliTimestamp, - ); -} - -pub fn saveKeyStore(a: std.mem.Allocator, pw: []const u8) !void { - // Get path to the users home folder - const home = try getHome(a); - defer a.free(home); - - // Open the passkee config folder - var config_path = try a.alloc(u8, home.len + CONFIG_DIR_NAME.len + 1); - @memcpy(config_path[0..home.len], home); - config_path[home.len] = '/'; - @memcpy(config_path[home.len + 1 ..], CONFIG_DIR_NAME); - defer a.free(config_path); - - var config_dir = try openConfigFolder(config_path); - - // Store key store - try writeFile(config_dir, "secrets.cks", &store.?, pw); -} - -fn getHome(a: std.mem.Allocator) ![]const u8 { - if (std.os.getenv("HOME")) |home| { - var d = try a.alloc(u8, home.len); - @memcpy(d, home); - return d; - } else { - return error.NotFound; + //notify.g_main_loop_run(loop); + while (!quit) { + _ = notify.g_main_context_iteration(context, 1); } -} - -pub fn openConfigFolder(path: []const u8) !std.fs.Dir { - return std.fs.openDirAbsolute(path, .{}) catch { - std.log.warn("Directory {s} doesn't exist. Try to create it...", .{path}); - try std.fs.makeDirAbsolute(path); - return try std.fs.openDirAbsolute(path, .{}); - }; -} - -pub fn loadFile(dir: std.fs.Dir, path: []const u8, a: std.mem.Allocator) ![]const u8 { - var file = try dir.openFile(path, .{ .mode = .read_write }); - return try file.readToEndAlloc(a, 128000); -} - -pub fn createFile(dir: std.fs.Dir, name: []const u8, pw: []const u8, a: std.mem.Allocator) !void { - // Test if file already exists - dir.access(name, .{ .mode = .read_write }) catch { - var o = try cks.CKS.new( - 1, - 0, - .ChaCha20, - .None, - .Argon2id, - "PassKee", - "PassKee-Secrets", - a, - std.crypto.random, - std.time.milliTimestamp, - ); - defer o.deinit(); - - try writeFile(dir, name, &o, pw); - return; - }; - return error.FileAlreadyExists; -} - -pub fn writeFile(dir: std.fs.Dir, path: []const u8, s: *cks.CKS, pw: []const u8) !void { - var file = dir.openFile(path, .{ .mode = .read_write }) catch blk: { - break :blk try dir.createFile(path, .{ .mode = 0o600 }); - }; - try file.setEndPos(0); - try s.seal(file.writer(), pw); -} - -/// This function MOST NOT be called if a `load` has failed! -pub fn get() *cks.CKS { - return &store.?; + _ = gpa.detectLeaks(); } // +++++++++++++++++++++++++++++++++++++++++++++ @@ -325,18 +235,6 @@ const LoadError = fido.ctap.authenticator.Callbacks.LoadError; const UpResult = fido.ctap.authenticator.Callbacks.UpResult; const UpReason = fido.ctap.authenticator.Callbacks.UpReason; -pub fn password(pw: ?[]const u8) ?[]const u8 { - const S = struct { - pub var s: ?[]const u8 = null; - }; - - if (pw != null) { - S.s = pw.?; - } - - return S.s; -} - /// Get the epoch time in ms pub fn millis() u64 { return @as(u64, @intCast(std.time.milliTimestamp())); @@ -400,33 +298,179 @@ pub fn up(reason: UpReason, user: ?*const fido.common.User, rp: ?*const fido.com pub fn reset() void {} -pub fn createEntry(id: []const u8) cks.Error!cks.Entry { - return try store.?.createEntry(id); -} +pub fn readSettings( + a: std.mem.Allocator, +) fido.ctap.authenticator.Callbacks.LoadError!fido.ctap.authenticator.Meta { + var buffer: [50000]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const all = fba.allocator(); + + var client = snorlax.Snorlax.init("127.0.0.1", 5984, couch_user.?, couch_pw.?, all) catch { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + }; + defer client.deinit(); -pub fn getEntry(id: []const u8) ?*cks.Entry { - return store.?.getEntry(id); + var meta = client.read(fido.ctap.authenticator.Meta, "passkee", "Settings", a) catch |err| { + if (err == error.NotFound) { + return fido.ctap.authenticator.Callbacks.LoadError.DoesNotExist; + } else { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + } + }; + return meta; } -pub fn getEntries( - filters: []const cks.Data.Filter, +pub fn updateSettings( + settings: *fido.ctap.authenticator.Meta, a: std.mem.Allocator, -) ?[]const *cks.Entry { // TODO: maybe rename to getResidentKeys - return store.?.getEntries(filters, a); +) fido.ctap.authenticator.Callbacks.StoreError!void { + var buffer: [50000]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const all = fba.allocator(); + + var client = snorlax.Snorlax.init("127.0.0.1", 5984, couch_user.?, couch_pw.?, all) catch { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + }; + defer client.deinit(); + + const x = client.update("passkee", settings, a) catch { + return fido.ctap.authenticator.Callbacks.StoreError.Other; + }; + + a.free(x.?.id); // we don't need this + if (settings._rev) |rev| { + // free old revision id + a.free(rev); + } + settings._rev = x.?.rev; } -pub fn addEntry(entry: cks.Entry) cks.Error!void { - try store.?.addEntry(entry); +pub fn readCred( + param: fido.ctap.authenticator.Callbacks.ReadCredParam, + a: std.mem.Allocator, +) fido.ctap.authenticator.Callbacks.LoadError![]fido.ctap.authenticator.Credential { + var buffer: [50000]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const all = fba.allocator(); + + var client = snorlax.Snorlax.init("127.0.0.1", 5984, couch_user.?, couch_pw.?, all) catch { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + }; + defer client.deinit(); + + var arr = std.ArrayList(fido.ctap.authenticator.Credential).init(a); + errdefer { + for (arr.items) |item| { + item.deinit(a); + } + arr.deinit(); + } + + switch (param) { + .id => |id| { + var meta = client.read(fido.ctap.authenticator.Credential, "passkee", id, a) catch |err| { + if (err == error.NotFound) { + return fido.ctap.authenticator.Callbacks.LoadError.DoesNotExist; + } else { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + } + }; + try arr.append(meta); + }, + .rpId => |id| { + const X = struct { + selector: struct { + rp_id: struct { + @"$eq": []const u8, + }, + }, + }; + const x = X{ .selector = .{ .rp_id = .{ .@"$eq" = id } } }; + + var creds = client.find("passkee", fido.ctap.authenticator.Credential, x, all) catch |err| { + if (err == error.NotFound) { + return fido.ctap.authenticator.Callbacks.LoadError.DoesNotExist; + } else { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + } + }; + defer creds.deinit(all); + + for (creds.docs) |d| { + try arr.append(try d.copy(a)); + } + }, + .all => |_| { + const X = struct { + selector: struct { + discoverable: struct { + @"$exists": bool, + }, + }, + }; + const x = X{ .selector = .{ .discoverable = .{ .@"$exists" = true } } }; + + var creds = client.find("passkee", fido.ctap.authenticator.Credential, x, all) catch |err| { + if (err == error.NotFound) { + return fido.ctap.authenticator.Callbacks.LoadError.DoesNotExist; + } else { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + } + }; + defer creds.deinit(all); + + for (creds.docs) |d| { + try arr.append(try d.copy(a)); + } + }, + } + + return try arr.toOwnedSlice(); } -pub fn removeEntry(id: []const u8) cks.Error!void { - try store.?.removeEntry(id); +pub fn updateCred( + cred: *fido.ctap.authenticator.Credential, + a: std.mem.Allocator, +) fido.ctap.authenticator.Callbacks.StoreError!void { + var buffer: [50000]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const all = fba.allocator(); + + var client = snorlax.Snorlax.init("127.0.0.1", 5984, couch_user.?, couch_pw.?, all) catch { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + }; + defer client.deinit(); + + const x = client.update("passkee", cred, a) catch { + return fido.ctap.authenticator.Callbacks.StoreError.Other; + }; + + a.free(x.?.id); // we don't need this + if (cred._rev) |rev| { + // free old revision id + a.free(rev); + } + cred._rev = x.?.rev; } -pub fn persist() error{Fatal}!void { - const pw = if (password(null)) |pw| pw else return error.Fatal; - saveKeyStore(allocator, pw) catch { - return error.Fatal; +pub fn deleteCred( + cred: *fido.ctap.authenticator.Credential, +) fido.ctap.authenticator.Callbacks.LoadError!void { + var buffer: [50000]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + const all = fba.allocator(); + + var client = snorlax.Snorlax.init("127.0.0.1", 5984, couch_user.?, couch_pw.?, all) catch { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + }; + defer client.deinit(); + + _ = client.delete("passkee", cred._id, cred._rev.?, null) catch |err| { + if (err == error.NotFound) { + return fido.ctap.authenticator.Callbacks.LoadError.DoesNotExist; + } else { + return fido.ctap.authenticator.Callbacks.LoadError.Other; + } }; } diff --git a/profiling_allocator/main.zig b/profiling_allocator/main.zig new file mode 100644 index 0000000..90acfa0 --- /dev/null +++ b/profiling_allocator/main.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const ProfilingAllocator = struct { + parent_allocator: Allocator, + map: std.AutoHashMap(usize, usize), + current_size: usize = 0, + max_size: usize = 0, + + const Self = @This(); + + pub fn init(parent_allocator: Allocator, second_allocator: Allocator) Self { + return .{ + .parent_allocator = parent_allocator, + .map = std.AutoHashMap(usize, usize).init(second_allocator), + }; + } + + pub fn allocator(self: *Self) Allocator { + return .{ + .ptr = self, + .vtable = &.{ + .alloc = alloc, + .resize = resize, + .free = free, + }, + }; + } + + pub fn printStats(self: *Self) void { + std.log.info("current_size={d}", .{self.current_size}); + } + + fn alloc( + ctx: *anyopaque, + len: usize, + log2_ptr_align: u8, + ra: usize, + ) ?[*]u8 { + const self: *Self = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawAlloc(len, log2_ptr_align, ra); + if (result != null) { + const addr = @intFromPtr(result); + self.map.put(addr, len) catch unreachable; + self.current_size += len; + + if (self.max_size < self.current_size) { + self.max_size = self.current_size; + } + } else { + std.log.err( + "alloc - failure: OutOfMemory - len: {}, ptr_align: {}", + .{ len, log2_ptr_align }, + ); + } + return result; + } + + fn resize( + ctx: *anyopaque, + buf: []u8, + log2_buf_align: u8, + new_len: usize, + ra: usize, + ) bool { + const self: *Self = @ptrCast(@alignCast(ctx)); + if (self.parent_allocator.rawResize(buf, log2_buf_align, new_len, ra)) { + const addr = @intFromPtr(&buf[0]); + const len = self.map.get(addr).?; + self.map.put(addr, new_len) catch unreachable; + + self.current_size -= len; + self.current_size += new_len; + + if (self.max_size < self.current_size) { + self.max_size = self.current_size; + } + + return true; + } + + std.debug.assert(new_len > buf.len); + std.log.err( + "expand - failure - {} to {}, buf_align: {}", + .{ buf.len, new_len, log2_buf_align }, + ); + return false; + } + + fn free( + ctx: *anyopaque, + buf: []u8, + log2_buf_align: u8, + ra: usize, + ) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.parent_allocator.rawFree(buf, log2_buf_align, ra); + const addr = @intFromPtr(&buf[0]); + const len = self.map.get(addr).?; + self.current_size -= len; + _ = self.map.remove(addr); + } +};