diff --git a/.envrc b/.envrc index 193c1ea..6ecf723 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,4 @@ use flake -ROOT="." +ROOT="/home/spencer/github.com/envr-zig" export PATH=".:${ROOT}/deps/zig:${ROOT}/deps/zls:$PATH" diff --git a/src/Config.zig b/src/Config.zig index a5efb64..2905bda 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -16,7 +16,10 @@ pub const SSHKeyPair = struct { public: []const u8, /// Caller owns the returned memory - pub fn from_path(gpa: std.mem.Allocator, path: []const u8) !SSHKeyPair { + pub fn from_path( + gpa: std.mem.Allocator, + path: []const u8, + ) error{OutOfMemory}!SSHKeyPair { if (std.mem.eql(u8, std.fs.path.extension(path), ".pub")) { return from_pub_path(path); } else { @@ -61,6 +64,7 @@ pub const ScanConfig = struct { }; /// Load the Config from the file at path +/// TODO: Use a concrete error set pub fn load( io: std.Io, gpa: std.mem.Allocator, diff --git a/src/main.zig b/src/main.zig index a4bc9eb..c6f693f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -34,13 +34,6 @@ fn run( return envr.root.help(stdout_writer); }, - .version => { - var stdout_buffer: [1024]u8 = undefined; - var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); - const stdout_writer = &stdout_file_writer.interface; - - return version(stdout_writer); - }, .deps => { var stdout_buffer: [1024]u8 = undefined; var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); @@ -52,6 +45,22 @@ fn run( environ_map.get("PATH").?, ); }, + .init => { + var stdout_buffer: [1024]u8 = undefined; + var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); + const stdout_writer = &stdout_file_writer.interface; + + try envr.init_cmd( + io, + arena, + stdout_writer, + environ_map.get("HOME").?, + .{ + // TODO: Actually parse this + .force = true, + }, + ); + }, .list => { var stdout_buffer: [page_size]u8 = undefined; var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); @@ -66,6 +75,13 @@ fn run( "/tmp", ); }, + .version => { + var stdout_buffer: [1024]u8 = undefined; + var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); + const stdout_writer = &stdout_file_writer.interface; + + return version(stdout_writer); + }, .unknown => { return fallback_to_go(io, arena, args); }, diff --git a/src/root.zig b/src/root.zig index b2812e1..c1d9021 100644 --- a/src/root.zig +++ b/src/root.zig @@ -52,6 +52,19 @@ pub const root: Command = .new(.{ \\ The deps command reports which binaries are available and which are not." , }, + .{ + .name = "init", + .short = "Set up envr", + .long = + \\The init command generates your initial config and saves it to + \\~/.envr/config in JSON format. + \\ + \\During setup, you will be prompted to select one or more ssh keys with which to + \\encrypt your databse. **Make 100% sure** that you have **a remote copy** of this + \\key somewhere, otherwise your data could be lost forever. + , + //.flags = struct { force: bool } + }, .{ .name = "list", .short = "View your tracked files", @@ -120,6 +133,144 @@ const Features = packed struct { } }; +pub fn init_cmd( + io: Io, + arena: std.mem.Allocator, + out: *std.Io.Writer, + home: []const u8, + flags: struct { force: bool }, +) !void { + defer out.flush() catch unreachable; + + // TODO: Don't hardcode + const cfgPath = try std.fs.path.join(arena, &.{ home, ".envr", "config.json" }); + defer arena.free(cfgPath); + + if (flags.force or !file_exists(io, cfgPath)) { + const keys = try select_ssh_keys(io, arena, home, out); + + // defer { + // for (keys) |*key| { + // arena.destroy(key); + // } + // arena.free(&keys); + // } + + // const cfg: Config = .{ .keys = keys }; + // TODO: How to handle this error? + // try cfg.save(io, cfgPath); + + try out.print( + "Config initialized with {} SSH key(s). You are ready to use envr.\n", + .{keys.len}, + ); + } else { + try out.writeAll( + \\You have already initialized envr. + \\Run again with the --force flag if you want to reinitialize. + \\ + , + ); + } +} + +/// Returns true if the file exists +fn file_exists(io: std.Io, path: []const u8) bool { + if (std.Io.Dir.cwd().access(io, path, .{ .read = true })) { + return true; + } else |_| { + return false; + } +} + +/// Returns a list of keys that the user has selected to add to their config. +/// Caller owns the returned memory +// TODO: Write a test for this +fn select_ssh_keys( + io: std.Io, + alloc: std.mem.Allocator, + home_path: []const u8, + out: *std.Io.Writer, +) ![]Config.SSHKeyPair { + const ssh_path = try std.fs.path.join(alloc, &.{ home_path, ".ssh" }); + defer alloc.free(ssh_path); + + // TODO: Arbitrary capacity chosen + var keys: std.ArrayList(Config.SSHKeyPair) = try .initCapacity(alloc, 3); + + { + const ssh_dir = try std.Io.Dir.cwd().openDir(io, ssh_path, .{ .iterate = true }); + defer ssh_dir.close(io); + + var itr = ssh_dir.iterate(); + + const expect1 = + \\-----BEGIN OPENSSH PRIVATE KEY----- + \\ + ; + + const expect2 = + \\-----BEGIN RSA PRIVATE KEY----- + \\ + ; + + var buf: [expect1.len]u8 = undefined; + + while (try itr.next(io)) |entry| { + switch (entry.kind) { + .file => { + var file = try ssh_dir.openFile(io, entry.name, .{}); + _ = try file.readPositionalAll(io, &buf, 0); + + // TODO: Faster to use hash or something? + if ( // zig fmt: off + std.mem.eql(u8, expect1, &buf) or + std.mem.eql(u8, expect2, buf[0..expect2.len]) + ) { // zig fmt: on + // File is a private ssh key + + const full_path = try ssh_dir.realPathFileAlloc( + io, + entry.name, + alloc, + ); + + try keys.append(alloc, try .from_path(alloc, full_path)); + } + }, + .sym_link => { + // TODO: Handle symlinks + }, + .block_device, + .character_device, + .directory, + .named_pipe, + .unix_domain_socket, + .whiteout, + .door, + .event_port, + .unknown, + => continue, + } + } + } + + for (keys.items, 1..) |key, n| { + try out.print("{d}. {s}\n", .{ n, key.private }); + } + try out.writeAll( + "\nPlease enter the number(s) of SSH keys you'd like to use for encryption:\n> ", + ); + try out.flush(); + defer out.writeAll("\n\n") catch unreachable; + + // TODO: ask user for number(s) to use. + // TODO: confirm with a y/n prompt + // TODO: only return selected keys + + return keys.toOwnedSlice(alloc); +} + pub fn list( io: Io, arena: std.mem.Allocator,