diff --git a/build.zig.zon b/build.zig.zon index 9f30ffd..11880fe 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -32,7 +32,9 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ + // .age = .{ .path = "zig-vendor/age-ffi/zig" }, .sqlite = .{ .path = "zig-vendor/zig-sqlite" }, + // See `zig fetch --save ` for a command-line interface for adding dependencies. //.example = .{ // // When updating this field to a new URL, be sure to delete the corresponding diff --git a/fixtures/encrypted-example.db.age b/fixtures/encrypted-example.db.age new file mode 100644 index 0000000..7a66592 Binary files /dev/null and b/fixtures/encrypted-example.db.age differ diff --git a/fixtures/hello-world.age b/fixtures/hello-world.age new file mode 100644 index 0000000..681af94 --- /dev/null +++ b/fixtures/hello-world.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 Boe0UQ 2ngx7jSJ8/yuAzTgeiiCTYZRSkBCeJfaHTL0u7k6ziU +0XmEy0bOTeW1MF9ev32n4xISPDl9UQNHzEB0vsZHDuU +--- UV7IjWFCCg79Pf3T9vUWBxT4MhgeARWp6E+LK9tMy1g +uNo2Z݅++Yy:@' NxP \ No newline at end of file diff --git a/fixtures/hello-world.txt b/fixtures/hello-world.txt new file mode 100644 index 0000000..8ab686e --- /dev/null +++ b/fixtures/hello-world.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/fixtures/insecure-test-key b/fixtures/insecure-test-key new file mode 100644 index 0000000..0f85442 --- /dev/null +++ b/fixtures/insecure-test-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryAAAAJCkxfzapMX8 +2gAAAAtzc2gtZWQyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryA +AAAEDXQExhs89b3fjqJHkhuo9QX4JEjXiEC+vSnCAYc8OxcpuWXQwml6v2ms/AafbDCKQf +eFMHIvy2XOaFWK69+uvIAAAACnNwZW5jZXJAZncBAgM= +-----END OPENSSH PRIVATE KEY----- diff --git a/fixtures/insecure-test-key.pub b/fixtures/insecure-test-key.pub new file mode 100644 index 0000000..8f8eb26 --- /dev/null +++ b/fixtures/insecure-test-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJuWXQwml6v2ms/AafbDCKQfeFMHIvy2XOaFWK69+uvI spencer@fw diff --git a/flake.nix b/flake.nix index 258a680..d2d0801 100644 --- a/flake.nix +++ b/flake.nix @@ -99,8 +99,9 @@ cobra-cli # Build tools - zip + age unstable.cargo + zip opencode diff --git a/src/age.zig b/src/age.zig new file mode 100644 index 0000000..7295a2c --- /dev/null +++ b/src/age.zig @@ -0,0 +1,123 @@ +const std = @import("std"); + +/// Returns the decrypted contents of the file. +/// Caller is responsible for freeing the memory. +pub fn decrypt( + io: std.Io, + gpa: std.mem.Allocator, + private_key: []const u8, + input_path: []const u8, + output_path: []const u8, +) !void { + const result = try std.process.run(gpa, io, .{ + .argv = &.{ + "age", + "-d", + "-i", + private_key, + "-o", + output_path, + input_path, + }, + }); + defer gpa.free(result.stderr); + defer gpa.free(result.stdout); + + if (result.stdout.len > 0) { + std.debug.print("stdout: \"{s}\"\n", .{result.stdout}); + unreachable; + } + + if (result.stderr.len > 0) { + std.debug.print("stderr: \"{s}\"\n", .{result.stderr}); + unreachable; + } +} + +/// Returns the encrypted contents of the file. +/// Caller is responsible for freeing the memory. +pub fn encrypt( + io: std.Io, + gpa: std.mem.Allocator, + public_key: []const u8, + input_path: []const u8, + output_path: []const u8, +) !void { + const result = try std.process.run(gpa, io, .{ + .argv = &.{ + "age", + "-e", + "-R", + public_key, + "-o", + output_path, + input_path, + }, + }); + defer gpa.free(result.stderr); + defer gpa.free(result.stdout); +} + +test "sample file can be decrypted" { + const io = std.testing.io; + const gpa = std.testing.allocator; + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa); + defer gpa.free(dir_path); + + const output_path = try std.fs.path.join(gpa, &.{ dir_path, "got.txt" }); + defer gpa.free(output_path); + + try decrypt( + io, + gpa, + "./fixtures/insecure-test-key", + "./fixtures/hello-world.age", + output_path, + ); + + const contents = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited); + defer gpa.free(contents); + + try std.testing.expectEqualSlices(u8, "Hello, World!\n", contents); +} + +test "sample file can be encrypted" { + const io = std.testing.io; + const gpa = std.testing.allocator; + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa); + defer gpa.free(dir_path); + + const output_path = try std.fs.path.join(gpa, &.{ dir_path, "hello-world.age" }); + defer gpa.free(output_path); + + try encrypt( + io, + gpa, + "./fixtures/insecure-test-key.pub", + "./fixtures/hello-world.txt", + output_path, + ); + + const got = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited); + defer gpa.free(got); + + const want = try std.Io.Dir.cwd().readFileAlloc( + io, + "./fixtures/hello-world.age", + gpa, + .unlimited, + ); + + const contents = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited); + defer gpa.free(contents); + + try std.testing.expectEqualSlices(u8, want, got); +} diff --git a/src/db.zig b/src/db.zig index eb70c4f..da1b749 100644 --- a/src/db.zig +++ b/src/db.zig @@ -1,6 +1,12 @@ const std = @import("std"); const sqlite = @import("sqlite"); +const age = @import("age.zig"); + +test { + std.testing.refAllDecls(@import("age.zig")); +} + test "simple database can be opened" { var db = try sqlite.Db.init(.{ .mode = sqlite.Db.Mode{ .File = "./fixtures/example.db" }, @@ -24,3 +30,47 @@ test "simple database can be opened" { return error.TestUnexpectedResult; } } + +test "encrypted database can be opened" { + const io = std.testing.io; + const gpa = std.testing.allocator; + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa); + defer gpa.free(dir_path); + + const decrypted_path = try std.fs.path.joinZ(gpa, &.{ dir_path, "example.db" }); + defer gpa.free(decrypted_path); + + try age.decrypt( + io, + gpa, + "./fixtures/insecure-test-key", + "./fixtures/encrypted-example.db.age", + decrypted_path, + ); + + var db = try sqlite.Db.init(.{ + .mode = sqlite.Db.Mode{ .File = decrypted_path }, + .open_flags = .{ + .write = false, + .create = false, + }, + .threading_mode = .MultiThread, + }); + + var stmt = try db.prepare("SELECT * FROM hello"); + defer stmt.deinit(); + + const alloc = std.testing.allocator; + + if (try stmt.oneAlloc(struct { text: []const u8 }, alloc, .{}, .{})) |got| { + defer alloc.free(got.text); + + try std.testing.expectEqualSlices(u8, "world!", got.text); + } else { + return error.TestUnexpectedResult; + } +} diff --git a/zig-vendor/age-ffi/zig/build.zig b/zig-vendor/age-ffi/zig/build.zig index a5968b3..8ea270a 100644 --- a/zig-vendor/age-ffi/zig/build.zig +++ b/zig-vendor/age-ffi/zig/build.zig @@ -25,9 +25,8 @@ pub fn build(b: *std.Build) void { // Link the Rust static library // Assumes the library has been built with: cargo build --release example.root_module.addLibraryPath(b.path("../target/release")); - example.root_module.linkSystemLibrary("age_ffi", .{}); - - // example.root_module.linkLibC(); + example.root_module.linkSystemLibrary("age_ffi", .{ .needed = true }); + // example.linkLibC(); // Install the example b.installArtifact(example); @@ -80,7 +79,7 @@ pub fn build(b: *std.Build) void { tests.root_module.addImport("age", age_module); tests.root_module.addLibraryPath(b.path("../target/release")); - tests.root_module.linkSystemLibrary("age_ffi", .{}); + tests.root_module.linkSystemLibrary("age_ffi", .{ .needed = true }); // tests.linkLibC(); tests.step.dependOn(&cargo_build.step); diff --git a/zig-vendor/age-ffi/zig/build.zig.zon b/zig-vendor/age-ffi/zig/build.zig.zon new file mode 100644 index 0000000..237128d --- /dev/null +++ b/zig-vendor/age-ffi/zig/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = .age, + .version = "0.1.0", + .fingerprint = 0xa13010b27f1528d3, + .minimum_zig_version = "0.14.0", + .paths = .{ + "zig", + }, +}