Viewing:
// 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);
}