From 11d75f0fb2cb0b508a24a52df4f3581b976abb2a Mon Sep 17 00:00:00 2001 From: "David Sugar (r4gus)" Date: Tue, 3 Sep 2024 11:31:15 +0200 Subject: [PATCH] command line application now allows to change the password --- build.zig | 17 ++++ build.zig.zon | 5 ++ src/cmd.zig | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/cmd.zig diff --git a/build.zig b/build.zig index 7219fa8..6a5b2a2 100644 --- a/build.zig +++ b/build.zig @@ -27,6 +27,11 @@ pub fn build(b: *std.Build) !void { }); const uuid_module = uuid_dep.module("uuid"); + const clap_dep = b.dependency("clap", .{ + .target = target, + .optimize = optimize, + }); + const lib = b.addStaticLibrary(.{ .name = "ccdb", // In this case the main source file is merely a path, however, in more @@ -120,4 +125,16 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); + + const cmd_exe = b.addExecutable(.{ + .name = "ccdbcmd", + .root_source_file = b.path("src/cmd.zig"), + .target = target, + .optimize = optimize, + }); + cmd_exe.root_module.addImport("zbor", zbor_module); + cmd_exe.root_module.addImport("ccdb", ccdb_module); + cmd_exe.root_module.addImport("clap", clap_dep.module("clap")); + cmd_exe.linkLibC(); + b.installArtifact(cmd_exe); } diff --git a/build.zig.zon b/build.zig.zon index 3c5f1f2..cee2089 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -52,6 +52,11 @@ .url = "https://github.com/r4gus/uuid-zig/archive/refs/tags/0.2.1.tar.gz", .hash = "1220b4deeb4ec1ec3493ea934905356384561b725dba69d1fbf6a25cb398716dd05b", }, + .clap = .{ + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", + .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", + //.path = "../ccdb", + }, }, // Specifies the set of files and directories that are included in this package. diff --git a/src/cmd.zig b/src/cmd.zig new file mode 100644 index 0000000..cdba6c9 --- /dev/null +++ b/src/cmd.zig @@ -0,0 +1,239 @@ +const std = @import("std"); +const clap = @import("clap"); +const ccdb = @import("ccdb"); +const cbor = @import("zbor"); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const allocator = gpa.allocator(); + +const help = + \\usage: ccdbcmd + \\ Display and modify the content of a CCDB credential database. + \\ Options are: + \\ -h, --help Display this help and exit. + \\ -l, --list List all credentials. + \\ -o, --open Open database file. + \\ -p, --password A password. This should be entered using command line substituation! + \\ -i, --index Index of an entry. + \\ -e, --export [JSON, CBOR] Export an entry using the specified file format. + \\ -c, --change Change password. + \\ + \\ Security considerations: + \\ The password file should only be readable by the user. Please do not enter your password + \\ on the command line as other users might be able to read it. + \\ + \\ Examples: + \\ `ccdbcmd -o ~/.passkeez/db.ccdb -p $(cat pw.txt) -i 0 -e CBOR` Export the given entry as CBOR + \\ +; + +pub fn main() !void { + const stdin = std.io.getStdIn(); + const stdout = std.io.getStdOut(); + const stderr = std.io.getStdErr(); + // --------------------------------------------------- + // Command Line Argument Parsing + // --------------------------------------------------- + var password: ?[]const u8 = null; + defer if (password) |pw| allocator.free(pw); + + const params = comptime clap.parseParamsComptime( + \\-h, --help Display this help and exit. + \\-l, --list List all credentials. + \\-o, --open Open database file. + \\-p, --password A password. This should be entered using command line substituation! + \\-i, --index Index of an entry. + \\-e, --export Export an entry using the specified file format. + \\-c, --change Change password. + \\ + ); + + var diag = clap.Diagnostic{}; + var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ + .diagnostic = &diag, + .allocator = gpa.allocator(), + }) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().writer(), err) catch {}; + return; + }; + defer res.deinit(); + + if (res.args.help != 0) { + try std.fmt.format(stdout.writer(), help, .{}); + return; + } + if (res.args.password) |p| { + password = try allocator.dupe(u8, p); + } + + // --------------------------------------------------- + // Load Database + // --------------------------------------------------- + if (res.args.open) |file| { + if (password == null) { + try std.fmt.format(stdout.writer(), "password: ", .{}); + password = try stdin.reader().readUntilDelimiterOrEofAlloc(allocator, '\n', 256); + } + + var database = open(file, password.?, allocator) catch |e| { + try std.fmt.format(stderr.writer(), "unable to open {s} ({any})\n", .{ file, e }); + return; + }; + defer database.deinit(); + + if (res.args.list != 0) { + for (database.body.entries.items, 0..) |entry, i| { + try std.fmt.format(stdout.writer(), "[{d}] {s}\n", .{ i, entry.uuid }); + if (entry.url) |url| { + try std.fmt.format(stdout.writer(), " url: {s}\n", .{url}); + } + if (entry.user) |user| { + if (user.display_name) |dn| { + try std.fmt.format(stdout.writer(), " user: {s}\n", .{dn}); + } else if (user.name) |n| { + try std.fmt.format(stdout.writer(), " user: {s}\n", .{n}); + } + } + } + } else if (res.args.@"export") |e| { + if (res.args.index) |i| { + if (database.body.entries.items.len <= i) { + try std.fmt.format(stderr.writer(), "index out of bounds\n", .{}); + return; + } + const entry = database.body.entries.items[i]; + + if (std.mem.eql(u8, "JSON", e) or std.mem.eql(u8, "json", e)) { + try serializeEntryToJson(&entry, stdout); + } else if (std.mem.eql(u8, "CBOR", e) or std.mem.eql(u8, "cbor", e)) { + var arr = std.ArrayList(u8).init(allocator); + defer arr.deinit(); + + try cbor.stringify(entry, .{}, arr.writer()); + try std.fmt.format(stdout.writer(), "{s}\n", .{std.fmt.fmtSliceHexLower(arr.items)}); + } else { + try std.fmt.format(stderr.writer(), "unsupported file format '{s}'\n", .{e}); + return; + } + } else { + try std.fmt.format(stderr.writer(), "operation requires index or uuid\n", .{}); + return; + } + } else if (res.args.change != 0) { + try std.fmt.format(stdout.writer(), "new password: ", .{}); + const new_password = try stdin.reader().readUntilDelimiterOrEofAlloc(allocator, '\n', 256); + if (new_password == null) { + try std.fmt.format(stderr.writer(), "no password entered\n", .{}); + return; + } + + try database.setKey(new_password.?); + + try writeDb(allocator, file, &database); + } + + return; + } +} + +fn serializeEntryToJson(entry: *const ccdb.Entry, stdout: anytype) !void { + try stdout.writer().writeAll("{\n"); + try std.fmt.format(stdout.writer(), " \"uuid\": \"{s}\"", .{entry.uuid}); + if (entry.name) |v| try std.fmt.format(stdout.writer(), ",\n \"name\": \"{s}\"", .{v}); + { + try stdout.writer().writeAll(",\n \"times\": {\n"); + try std.fmt.format(stdout.writer(), " \"creat\": {d},\n", .{entry.times.creat}); + try std.fmt.format(stdout.writer(), " \"mod\": {d}", .{entry.times.mod}); + if (entry.times.exp) |exp| try std.fmt.format(stdout.writer(), ",\n \"exp\": {d}", .{exp}); + if (entry.times.cnt) |cnt| try std.fmt.format(stdout.writer(), ",\n \"cnt\": {d}", .{cnt}); + try stdout.writer().writeAll("\n }"); + } + if (entry.notes) |v| try std.fmt.format(stdout.writer(), ",\n \"notes\": \"{s}\"", .{v}); + if (entry.secret) |v| try std.fmt.format(stdout.writer(), ",\n \"secret\": \"{s}\"", .{std.fmt.fmtSliceHexLower(v)}); + if (entry.key) |v| { + try std.fmt.format(stdout.writer(), ",\n \"key\": ", .{}); + try std.json.stringify(v, .{}, stdout.writer()); + } + if (entry.url) |v| try std.fmt.format(stdout.writer(), ",\n \"url\": \"{s}\"", .{v}); + if (entry.user) |user| { + try stdout.writer().writeAll(",\n \"user\": {\n"); + if (user.id) |id| try std.fmt.format(stdout.writer(), " \"id\": \"{s}\"", .{std.fmt.fmtSliceHexLower(id)}); + if (user.name) |name| try std.fmt.format(stdout.writer(), ",\n \"name\": \"{s}\"", .{name}); + if (user.display_name) |name| try std.fmt.format(stdout.writer(), ",\n \"display_name\": \"{s}\"", .{name}); + try stdout.writer().writeAll("\n }"); + } + // We'll ignore any relationships + //if (entry.group) |v| try std.fmt.format(stdout.writer(), ",\n \"group\": \"{s}\"", .{v}); + if (entry.tags) |v| { + try std.fmt.format(stdout.writer(), ",\n \"tags\": [", .{}); + for (v, 0..) |t, j| { + if (j > 0) try stdout.writer().writeAll(","); + try std.fmt.format(stdout.writer(), "\n \"{s}\"", .{t}); + } + try stdout.writer().writeAll("\n ]"); + } + if (entry.attach) |v| { + try std.fmt.format(stdout.writer(), ",\n \"attach\": [", .{}); + for (v, 0..) |attachment, j| { + if (j > 0) try stdout.writer().writeAll(","); + try stdout.writer().writeAll("\n {\n"); + try std.fmt.format(stdout.writer(), " \"desc\" \"{s}\",\n", .{attachment.desc}); + try std.fmt.format(stdout.writer(), " \"att\" \"{s}\"\n", .{std.fmt.fmtSliceHexLower(attachment.att)}); + try stdout.writer().writeAll(" }"); + } + try stdout.writer().writeAll("\n ]"); + } + try stdout.writer().writeAll("\n}\n"); +} + +pub fn open2(path: []const u8) !std.fs.File { + return try std.fs.openFileAbsolute(path[0..], .{ + .mode = .read_write, + .lock = .exclusive, + .lock_nonblocking = true, + }); +} + +pub fn open(path: []const u8, pw: []const u8, a: std.mem.Allocator) !ccdb.Db { + const file = try open2(path); + defer file.close(); + + const mem = try file.readToEndAlloc(a, 50_000_000); + defer a.free(mem); + + return ccdb.Db.open( + mem, + a, + std.time.milliTimestamp, + std.crypto.random, + pw, + ); +} + +pub fn writeDb(a: std.mem.Allocator, path: []const u8, database: *ccdb.Db) !void { + var f2 = std.fs.createFileAbsolute("/tmp/db.trs", .{ .truncate = true }) catch |e| { + std.log.err("unable to open temporary file in /tmp", .{}); + return e; + }; + defer f2.close(); + + const raw = database.seal(a) catch |e| { + std.log.err("unable to seal database ({any})", .{e}); + return e; + }; + defer { + @memset(raw, 0); + a.free(raw); + } + + f2.writer().writeAll(raw) catch |e| { + std.log.err("unable to persist database ({any})", .{e}); + return e; + }; + + std.fs.copyFileAbsolute("/tmp/db.trs", path, .{}) catch |e| { + std.log.err("unable to overwrite file `{s}`", .{path}); + return e; + }; +}