// Tool to import, export, or read from a password protected json file that can have nested data const std = @import("std"); const assert = std.debug.assert; const json = std.json; const mem = std.mem; const aegis256 = std.crypto.aead.aegis.Aegis256; const tag_length = aegis256.tag_length; const usage = \\USAGE: \\jsoncred # prints this usage \\jsoncred -h # prints this usage \\jsoncred --help # prints this usage \\jsoncred <file> <command> <path> # basic format of command \\<command> is one of {i[mport], e[xport], p[rint]} \\jsoncred <file> # print whole file to stdout \\jsoncred <file> print # print top level keys \\jsoncred <file> print <a.b.c> # print path a.b.c of <file> to stdout \\jsoncred <file> import <path> # import <file> to <path on disk> \\jsoncred <file> import # import <file> to <file.enc> \\jsoncred <file> export <path> # export <file> to <path on disk> (- for stdout) \\jsoncred <file> export # export <file> to stdout (e.g. print it) \\ \\jsoncred will read password from env var JSONCRED_PASSWORD \\or prompt for one if none is available (not yet hidden). \\ \\set JSONCRED_DEBUG=true env var for verbose output ; fn validateCommand(command: []const u8) CommandError![]const u8 { if (mem.eql(u8, command, "") or mem.eql(u8, command, "p")) { return "print"; } if (mem.eql(u8, command, "import") or mem.eql(u8, command, "export") or mem.eql(u8, command, "print")) { return command; } if (mem.eql(u8, command, "i")) { return "import"; } if (mem.eql(u8, command, "e")) { return "export"; } return CommandError.NoSuchCommand; } fn getFileBytes(all: mem.Allocator, path: []const u8) ![]const u8 { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const file_size: u64 = try file.getEndPos(); const buffer: []u8 = try all.alloc(u8, file_size); _ = try file.readAll(buffer); return buffer; } pub fn doPrint(rootValue: json.Value, path: []const u8, all: mem.Allocator) ![]u8 { const allocPrint = std.fmt.allocPrint; var iterValue = rootValue; var pathSegments = mem.splitScalar(u8, path, '.'); while (pathSegments.next()) |segment| { if (mem.eql(u8, segment, "")) { continue; } switch (iterValue) { .null, .bool, .integer, .float, .number_string, .string, .array => { break; }, .object => |x| { const getOptional = x.get(segment); if (getOptional) |y| { iterValue = y; } else { return allocPrint(all, "no such path segment {s}", .{ .x = segment }); } }, } } switch (iterValue) { .null => { return allocPrint(all, "null", .{}); }, .bool => |x| { return allocPrint(all, "{}", .{ .x = x }); }, .integer => |x| { return allocPrint(all, "{}", .{ .x = x }); }, .float => |x| { return allocPrint(all, "{}", .{ .x = x }); }, .number_string => |x| { return allocPrint(all, "{s}", .{ .x = x }); }, .string => |x| { return allocPrint(all, "{s}", .{ .x = x }); }, .array => |x| { return allocPrint(all, "{any}", .{ .x = x }); }, .object => |x| { var out = std.ArrayList(u8).init(all); defer out.deinit(); const bytesWriter = out.writer(); try bytesWriter.print("KEYS:\n", .{}); for (x.keys()) |k| { try bytesWriter.print("{s}\n", .{k}); } return allocPrint(all, "{s}", .{out.items}); }, } } pub fn main() !void { const stdout_file = std.io.getStdOut().writer(); var bw = std.io.bufferedWriter(stdout_file); const stdout = bw.writer(); defer { bw.flush() catch |err| switch (err) { else => {}, }; } var i: i64 = 0; var file: []const u8 = ""; var command: []const u8 = ""; var path: []const u8 = ""; var args_it = std.process.args(); var gpaa = std.heap.GeneralPurposeAllocator(.{}){}; const gpa = gpaa.allocator(); var password = std.process.getEnvVarOwned(gpa, "JSONCRED_PASSWORD") catch |err| switch (err) { else => "", }; const debugStr = std.process.getEnvVarOwned(gpa, "JSONCRED_DEBUG") catch |err| switch (err) { else => "", }; const debug = mem.eql(u8, debugStr, "true"); while (args_it.next()) |entry| { if (mem.eql(u8, entry, "-h") or mem.eql(u8, entry, "--help")) { try stdout.print("{s}\n", .{ .x = usage }); return; } if (i == 1) { file = entry; } if (i == 2) { command = entry; } if (i == 3) { path = entry; } i += 1; } if (i == 1) { try stdout.print("{s}\n", .{ .x = usage }); return; } if (i == 2) { command = "export"; path = "-"; } if (mem.eql(u8, password, "")) { var password_slice = [_]u8{0} ** 32; var ip: usize = 0; var stdin_buf = std.io.bufferedReader(std.io.getStdIn().reader()); var line_buf = std.ArrayList(u8).init(gpa); defer line_buf.deinit(); std.debug.print("Enter password (must not not be more than 32 characters):\n", .{}); while (stdin_buf.reader().streamUntilDelimiter(line_buf.writer(), '\n', null)) { if (line_buf.getLastOrNull() == '\r') _ = line_buf.pop(); break; } else |err| switch (err) { error.EndOfStream => {}, else => |_| {}, } if (line_buf.items.len > 32) { std.debug.print("key may not be more than 32 characters\n", .{}); std.process.exit(1); } for (line_buf.items) |letter| { password_slice[ip] = letter; ip += 1; } password = &password_slice; } command = validateCommand(command) catch |err| switch (err) { CommandError.NoSuchCommand => { std.debug.print("command not found: {s}\n {s}\n", .{ command, usage }); std.process.exit(1); }, }; const is_export = mem.eql(u8, command, "export"); const is_print = !is_export and mem.eql(u8, command, "print"); const is_import = !is_export and !is_print and mem.eql(u8, command, "import"); const new_path: []u8 = try gpa.alloc(u8, file.len + 4); defer gpa.free(new_path); if (is_import and mem.eql(u8, path, "")) { path = try std.fmt.bufPrint(new_path, "{s}{s}", .{ file, ".enc" }); } if (debug) { try stdout.print("config:\n", .{}); try stdout.print("file: {s}\n", .{ .x = file }); try stdout.print("command: {s}\n", .{ .x = command }); try stdout.print("path: {s}\n", .{ .x = path }); try stdout.print("password: {}\n", .{ .x = password.len }); try stdout.print("debug: {}\n\n", .{ .x = debug }); try bw.flush(); } const fileBytes = getFileBytes(gpa, file) catch |err| { try stdout.print("could not get file {s}: {any}\n", .{ file, err }); try bw.flush(); std.process.exit(1); }; defer gpa.free(fileBytes); if (is_import) { const encrypted = try encrypt(fileBytes, password, gpa); defer gpa.free(encrypted); const encrypted_and_tag: []u8 = try gpa.alloc(u8, encrypted.len + tag_length); defer gpa.free(encrypted_and_tag); @memcpy(encrypted_and_tag[0..encrypted.len], encrypted); @memcpy(encrypted_and_tag[encrypted.len..], &tag); std.fs.cwd().writeFile(.{ .data = encrypted_and_tag, .sub_path = path }) catch |err| { try stdout.print("could not write file when importing: {any}\n", .{err}); try bw.flush(); std.process.exit(1); }; if (debug) { try stdout.print("encrypted {any}\n", .{ .x = encrypted_and_tag }); } return; } if (is_export or is_print) { if (debug) { try stdout.print("to_decrypt {any}\n", .{ .x = fileBytes }); } const decrypted = decrypt(fileBytes[0 .. fileBytes.len - tag_length], fileBytes[fileBytes.len - tag_length ..], password, gpa) catch |err| { try stdout.print("decrypt failed: {any}\n", .{err}); try bw.flush(); std.process.exit(1); }; defer gpa.free(decrypted); if (is_export) { if (mem.eql(u8, path, "-") or mem.eql(u8, path, ".")) { try stdout.print("{s}", .{ .x = decrypted }); return; } std.fs.cwd().writeFile(.{ .data = decrypted, .sub_path = path }) catch |err| { try stdout.print("could not write file when importing: {any}\n", .{err}); try bw.flush(); std.process.exit(1); }; return; } const parsed = try json.parseFromSlice(json.Value, gpa, decrypted, .{}); defer parsed.deinit(); const rootValue = parsed.value; const printValue = try doPrint(rootValue, path, gpa); defer gpa.free(printValue); try stdout.print("{s}\n", .{printValue}); } } const ad = "asdf"; var tag = [_]u8{0} ** 16; const npub = [_]u8{0} ** 32; fn encrypt(data: []const u8, key: []const u8, all: mem.Allocator) ![]u8 { assert(key.len <= 32); var key32 = [_]u8{0} ** 32; var i: usize = 0; for (key) |letter| { key32[i] = letter; i += 1; } const encrypted_data: []u8 = try all.alloc(u8, data.len); aegis256.encrypt(encrypted_data, &tag, data, ad, npub, key32); return encrypted_data; } fn decrypt(data: []const u8, decrypt_tag: []const u8, key: []const u8, all: mem.Allocator) ![]u8 { assert(key.len <= 32); var key32 = [_]u8{0} ** 32; var i: usize = 0; for (key) |letter| { key32[i] = letter; i += 1; } var the_tag = [_]u8{0} ** 16; var j: usize = 0; for (decrypt_tag) |byt| { the_tag[j] = byt; j += 1; } const decrypted_data: []u8 = try all.alloc(u8, data.len); try aegis256.decrypt(decrypted_data, data, the_tag, ad, npub, key32); return decrypted_data; } const CommandError = error{ NoSuchCommand, }; test "validate command" { const expect = std.testing.expect; // const expectError = std.testing.expectError; var actual = try validateCommand("print"); try expect(mem.eql(u8, actual, "print")); actual = try validateCommand("p"); try expect(mem.eql(u8, actual, "print")); actual = try validateCommand(""); try expect(mem.eql(u8, actual, "print")); actual = try validateCommand("export"); try expect(mem.eql(u8, actual, "export")); actual = try validateCommand("e"); try expect(mem.eql(u8, actual, "export")); actual = try validateCommand("import"); try expect(mem.eql(u8, actual, "import")); actual = try validateCommand("i"); try expect(mem.eql(u8, actual, "import")); const myErr = validateCommand("unknown"); try expect(myErr == CommandError.NoSuchCommand); } test "print primitive number" { // const expect = std.testing.expect; const expectEqualStrings = std.testing.expectEqualStrings; const alloc = std.testing.allocator; const parsed = try json.parseFromSlice(json.Value, alloc, "3", .{}); defer parsed.deinit(); const printValue = try doPrint(parsed.value, "", alloc); defer alloc.free(printValue); try expectEqualStrings(printValue, "3"); } test "print deep object" { const expectEqualStrings = std.testing.expectEqualStrings; const alloc = std.testing.allocator; const deepObjStr = \\{"a": {"b": 3}} ; const parsed = try json.parseFromSlice(json.Value, alloc, deepObjStr, .{}); defer parsed.deinit(); const printValue = try doPrint(parsed.value, "a.b", alloc); defer alloc.free(printValue); try expectEqualStrings(printValue, "3"); } test "print deep object keys" { const expectEqualStrings = std.testing.expectEqualStrings; const alloc = std.testing.allocator; const deepObjStr = \\{"a": "a", "b": 3} ; const parsed = try json.parseFromSlice(json.Value, alloc, deepObjStr, .{}); defer parsed.deinit(); const printValue = try doPrint(parsed.value, "", alloc); defer alloc.free(printValue); const expected = \\KEYS: \\a \\b \\ ; try expectEqualStrings(printValue, expected); }