Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/cli/commands/unlock.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const terminal = @import("../terminal.zig");
const memory = @import("../../memory/secure_allocator.zig");
const agent = @import("../../services/agent.zig");

pub fn run(allocator: std.mem.Allocator, args: []const []const u8, _: config.Config) !void {
pub fn run(allocator: std.mem.Allocator, args: []const []const u8, cfg: config.Config) !void {
for (args) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
showHelp();
Expand Down Expand Up @@ -75,7 +75,7 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8, _: config.Con
return;
};

var server = agent.AgentServer.init(allocator, derived_key) catch {
var server = agent.AgentServer.init(allocator, derived_key, cfg.auto_lock_minutes) catch {
std.debug.print("Error: Could not initialize agent server\n", .{});
return;
};
Expand All @@ -94,6 +94,9 @@ pub fn run(allocator: std.mem.Allocator, args: []const []const u8, _: config.Con
};

std.debug.print("Vault unlocked. Agent running.\n", .{});
if (cfg.auto_lock_minutes > 0) {
std.debug.print("Auto-lock enabled: {d} minutes of inactivity.\n", .{cfg.auto_lock_minutes});
}

if (foreground) {
std.debug.print("Press Ctrl+C to lock and exit.\n", .{});
Expand Down Expand Up @@ -134,6 +137,10 @@ fn showHelp() void {
\\encryption key in memory, so subsequent commands don't require
\\entering the master password.
\\
\\The vault will automatically lock after a period of inactivity
\\(default: 30 minutes). Configure with [security] auto_lock_minutes
\\in config.toml. Set to 0 to disable auto-lock.
\\
\\Options:
\\ -f, --foreground Run agent in foreground (don't daemonize)
\\ -h, --help Show this help message
Expand Down
27 changes: 26 additions & 1 deletion src/core/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ pub const Config = struct {

fn applySecurityValue(config: *Self, key: []const u8, value: []const u8) void {
if (std.mem.eql(u8, key, "auto_lock_minutes")) {
config.auto_lock_minutes = parseU32(value) orelse config.auto_lock_minutes;
if (parseU32(value)) |minutes| {
// Cap at 24 hours (1440 minutes) to prevent unreasonable values
config.auto_lock_minutes = if (minutes > 1440) 1440 else minutes;
}
}
}

Expand Down Expand Up @@ -316,3 +319,25 @@ test "config path generation" {

try std.testing.expect(std.mem.endsWith(u8, config_path, "zault/config.toml"));
}

test "config auto_lock_minutes capped at 24 hours" {
const content =
\\[security]
\\auto_lock_minutes = 9999
;

const config = Config.parse(content);

try std.testing.expectEqual(@as(u32, 1440), config.auto_lock_minutes);
}

test "config auto_lock_minutes accepts valid values" {
const content =
\\[security]
\\auto_lock_minutes = 60
;

const config = Config.parse(content);

try std.testing.expectEqual(@as(u32, 60), config.auto_lock_minutes);
}
111 changes: 105 additions & 6 deletions src/services/agent.zig
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,27 @@ pub const AgentServer = struct {
derived_key: [KEY_LENGTH]u8,
server: ?std.posix.socket_t,
running: bool,
auto_lock_ns: ?i128,
last_activity: i128,

const Self = @This();

pub fn init(allocator: std.mem.Allocator, derived_key: [KEY_LENGTH]u8) !Self {
pub fn init(allocator: std.mem.Allocator, derived_key: [KEY_LENGTH]u8, auto_lock_minutes: u32) !Self {
const socket_path = try getSocketPath(allocator);

const auto_lock_ns: ?i128 = if (auto_lock_minutes == 0)
null
else
@as(i128, auto_lock_minutes) * 60 * std.time.ns_per_s;

const self = Self{
.allocator = allocator,
.socket_path = socket_path,
.derived_key = derived_key,
.server = null,
.running = false,
.auto_lock_ns = auto_lock_ns,
.last_activity = std.time.nanoTimestamp(),
};

return self;
Expand Down Expand Up @@ -138,26 +147,73 @@ pub const AgentServer = struct {
pub fn runLoop(self: *Self) void {
const server = self.server orelse return;

// Poll timeout balances auto-lock responsiveness with CPU efficiency.
// Shorter = faster auto-lock detection, longer = less CPU wake-ups.
const poll_timeout_ms: i32 = 10_000;
var consecutive_poll_errors: u32 = 0;

while (self.running) {
if (self.auto_lock_ns) |timeout_ns| {
const now = std.time.nanoTimestamp();
const elapsed = now - self.last_activity;

// Guard against clock skew (NTP correction, VM resume, etc.)
if (elapsed < 0) {
self.last_activity = now;
continue;
}

if (elapsed >= timeout_ns) {
memory.secureZero(&self.derived_key);
self.running = false;
break;
}
}

var fds = [_]std.posix.pollfd{
.{ .fd = server, .events = std.posix.POLL.IN, .revents = 0 },
};

const poll_result = std.posix.poll(&fds, poll_timeout_ms) catch {
consecutive_poll_errors += 1;
if (consecutive_poll_errors > 10) {
memory.secureZero(&self.derived_key);
self.running = false;
break;
}
continue;
};
consecutive_poll_errors = 0;

if (poll_result == 0) continue;

if (fds[0].revents & std.posix.POLL.IN == 0) continue;

var client_addr: std.posix.sockaddr = undefined;
var addr_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr);

const client = std.posix.accept(server, &client_addr, &addr_len, 0) catch continue;
defer std.posix.close(client);

self.handleClient(client);
const is_active_command = self.handleClient(client);
if (is_active_command) {
self.last_activity = std.time.nanoTimestamp();
}
}
}

fn handleClient(self: *Self, client: std.posix.socket_t) void {
/// Handle a client request. Returns true if this was an active command
/// that should reset the auto-lock timer (GET_KEY), false for passive
/// commands (STATUS) or explicit lock requests.
fn handleClient(self: *Self, client: std.posix.socket_t) bool {
if (!self.verifyPeerCredentials(client)) {
_ = std.posix.write(client, &[_]u8{@intFromEnum(Response.err)}) catch {};
return;
return false;
}

var buf: [32]u8 = undefined;
const n = std.posix.read(client, &buf) catch return;
if (n == 0) return;
const n = std.posix.read(client, &buf) catch return false;
if (n == 0) return false;

const cmd_line = std.mem.trimRight(u8, buf[0..n], "\n\r");

Expand All @@ -166,13 +222,18 @@ pub const AgentServer = struct {
response[0] = @intFromEnum(Response.key);
@memcpy(response[1..], &self.derived_key);
_ = std.posix.write(client, &response) catch {};
return true;
} else if (std.mem.eql(u8, cmd_line, "LOCK")) {
_ = std.posix.write(client, &[_]u8{@intFromEnum(Response.ok)}) catch {};
memory.secureZero(&self.derived_key);
self.running = false;
return false;
} else if (std.mem.eql(u8, cmd_line, "STATUS")) {
_ = std.posix.write(client, &[_]u8{@intFromEnum(Response.unlocked)}) catch {};
return false;
} else {
_ = std.posix.write(client, &[_]u8{@intFromEnum(Response.err)}) catch {};
return false;
}
}

Expand Down Expand Up @@ -305,3 +366,41 @@ test "socket path generation" {
try std.testing.expect(path.len > 0);
try std.testing.expect(std.mem.endsWith(u8, path, "agent.sock"));
}

test "auto-lock timeout calculation" {
const allocator = std.testing.allocator;
var key: [KEY_LENGTH]u8 = undefined;
@memset(&key, 0);

var server_30min = try AgentServer.init(allocator, key, 30);
defer server_30min.deinit();
const expected_30min: i128 = 30 * 60 * std.time.ns_per_s;
try std.testing.expectEqual(expected_30min, server_30min.auto_lock_ns.?);

var server_1min = try AgentServer.init(allocator, key, 1);
defer server_1min.deinit();
const expected_1min: i128 = 1 * 60 * std.time.ns_per_s;
try std.testing.expectEqual(expected_1min, server_1min.auto_lock_ns.?);
}

test "auto-lock disabled when zero" {
const allocator = std.testing.allocator;
var key: [KEY_LENGTH]u8 = undefined;
@memset(&key, 0);

var server = try AgentServer.init(allocator, key, 0);
defer server.deinit();
try std.testing.expectEqual(@as(?i128, null), server.auto_lock_ns);
}

test "auto-lock handles max timeout value" {
const allocator = std.testing.allocator;
var key: [KEY_LENGTH]u8 = undefined;
@memset(&key, 0);

var server = try AgentServer.init(allocator, key, 1440);
defer server.deinit();

const expected_ns: i128 = 1440 * 60 * std.time.ns_per_s;
try std.testing.expectEqual(expected_ns, server.auto_lock_ns.?);
}