main.zig (9264B)
1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 4 const Entry = struct { 5 path: []const u8, 6 accessed: i128, 7 }; 8 9 pub fn main(init: std.process.Init) !void { 10 const alloc = init.gpa; 11 12 const pass_dir = try dir(alloc, init.environ_map); 13 defer alloc.free(pass_dir); 14 15 var passwords = std.ArrayListUnmanaged(Entry).empty; 16 defer { 17 for (passwords.items) |p| alloc.free(p.path); 18 passwords.deinit(alloc); 19 } 20 21 try collect(alloc, init.io, pass_dir, "", &passwords); 22 23 std.mem.sort(Entry, passwords.items, {}, struct { 24 fn lessThan(_: void, a: Entry, b: Entry) bool { 25 return a.accessed > b.accessed; 26 } 27 }.lessThan); 28 29 var paths = std.ArrayListUnmanaged([]const u8).empty; 30 defer paths.deinit(alloc); 31 for (passwords.items) |entry| { 32 try paths.append(alloc, entry.path); 33 } 34 35 const selected = try fzf(init, alloc, paths.items) orelse return; 36 defer alloc.free(selected); 37 38 const exists = blk: { 39 for (passwords.items) |item| { 40 if (std.mem.eql(u8, item.path, selected)) break :blk true; 41 } 42 break :blk false; 43 }; 44 const basename_start = if (std.mem.lastIndexOfScalar(u8, selected, '/')) |idx| idx + 1 else 0; 45 const has_otp = std.mem.find(u8, selected[basename_start..], "otp") != null; 46 47 const secret = if (has_otp and exists) 48 try run(init, alloc, &.{ "pass", "otp", "code", selected }) 49 else if (!exists) 50 try store(init, alloc, selected) 51 else 52 run(init, alloc, &.{ "pass", "show", selected }) catch try store(init, alloc, selected); 53 54 try clip(init, secret); 55 alloc.free(secret); 56 } 57 58 fn dir(alloc: std.mem.Allocator, environ: *std.process.Environ.Map) ![]const u8 { 59 if (environ.get("PASSWORD_STORE_DIR")) |p| { 60 return try alloc.dupe(u8, p); 61 } 62 63 const home = environ.get("HOME") orelse return error.NoHome; 64 return try std.fs.path.join(alloc, &.{ home, ".password-store" }); 65 } 66 67 fn collect(alloc: std.mem.Allocator, io: std.Io, base_dir: []const u8, sub_path: []const u8, list: *std.ArrayListUnmanaged(Entry)) anyerror!void { 68 const full_path = try std.fs.path.join(alloc, &.{ base_dir, sub_path }); 69 defer alloc.free(full_path); 70 71 var d = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch return; 72 defer d.close(io); 73 74 var it = d.iterate(); 75 while (try it.next(io)) |entry| { 76 if (entry.name.len > 0 and entry.name[0] == '.') continue; 77 78 const entry_sub_path = if (sub_path.len == 0) 79 try alloc.dupe(u8, entry.name) 80 else 81 try std.fs.path.join(alloc, &.{ sub_path, entry.name }); 82 errdefer alloc.free(entry_sub_path); 83 84 if (entry.kind == .directory) { 85 try collect(alloc, io, base_dir, entry_sub_path, list); 86 alloc.free(entry_sub_path); 87 } else if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".gpg")) { 88 const name = entry_sub_path[0 .. entry_sub_path.len - 4]; 89 const file_path = try std.fs.path.join(alloc, &.{ base_dir, entry_sub_path }); 90 defer alloc.free(file_path); 91 92 const accessed = blk: { 93 const file = std.Io.Dir.openFileAbsolute(io, file_path, .{}) catch |err| { 94 std.debug.print("Failed to stat {s}: {}\n", .{ file_path, err }); 95 break :blk @as(i128, 0); 96 }; 97 defer file.close(io); 98 99 const file_stat = file.stat(io) catch |err| { 100 std.debug.print("Failed to stat {s}: {}\n", .{ file_path, err }); 101 break :blk @as(i128, 0); 102 }; 103 104 const atime_ts = file_stat.atime orelse break :blk @as(i128, 0); 105 break :blk @as(i128, @intCast(atime_ts.nanoseconds)); 106 }; 107 108 try list.append(alloc, Entry{ 109 .path = try alloc.dupe(u8, name), 110 .accessed = accessed, 111 }); 112 alloc.free(entry_sub_path); 113 } else { 114 alloc.free(entry_sub_path); 115 } 116 } 117 } 118 119 fn fzf(init: std.process.Init, alloc: std.mem.Allocator, items: []const []const u8) !?[]u8 { 120 var child = try std.process.spawn(init.io, .{ 121 .argv = &.{ "fzf", "--print-query" }, 122 .stdin = .pipe, 123 .stdout = .pipe, 124 .stderr = .inherit, 125 }); 126 127 if (child.stdin) |stdin| { 128 var buf: [4096]u8 = undefined; 129 var fw = stdin.writer(init.io, &buf); 130 for (items) |item| { 131 try fw.interface.writeAll(item); 132 try fw.interface.writeAll("\n"); 133 } 134 try fw.flush(); 135 stdin.close(init.io); 136 child.stdin = null; 137 } 138 139 const output = try read(init.io, alloc, &child); 140 defer alloc.free(output); 141 142 const term = try child.wait(init.io); 143 if (term != .exited) return null; 144 145 const trimmed = std.mem.trim(u8, output, " \n\r\t"); 146 if (trimmed.len == 0) return null; 147 148 if (std.mem.findScalar(u8, trimmed, '\n')) |idx| { 149 const selected = std.mem.trim(u8, trimmed[idx + 1 ..], " \n\r\t"); 150 if (selected.len > 0) return try alloc.dupe(u8, selected); 151 const query = std.mem.trim(u8, trimmed[0..idx], " \n\r\t"); 152 if (query.len > 0) return try alloc.dupe(u8, query); 153 } 154 155 return try alloc.dupe(u8, trimmed); 156 } 157 158 fn run(init: std.process.Init, alloc: std.mem.Allocator, args: []const []const u8) ![]u8 { 159 var child = try std.process.spawn(init.io, .{ 160 .argv = args, 161 .stdout = .pipe, 162 .stderr = .inherit, 163 }); 164 165 const output = try read(init.io, alloc, &child); 166 167 const term = try child.wait(init.io); 168 if (term != .exited or term.exited != 0) { 169 alloc.free(output); 170 return error.FailedExit; 171 } 172 173 if (std.mem.findScalar(u8, output, '\n')) |idx| { 174 const result = try alloc.dupe(u8, output[0..idx]); 175 alloc.free(output); 176 return result; 177 } 178 179 return output; 180 } 181 182 fn read(io: std.Io, alloc: std.mem.Allocator, child: *std.process.Child) ![]u8 { 183 if (child.stdout) |file| { 184 var buf: [4096]u8 = undefined; 185 var fr = file.reader(io, &buf); 186 return try fr.interface.allocRemaining(alloc, .unlimited); 187 } 188 return try alloc.dupe(u8, ""); 189 } 190 191 fn gen(init: std.process.Init, alloc: std.mem.Allocator) ![]u8 { 192 const charset = std.ascii.letters ++ "0123456789"; 193 const special_chars = "!@#$%^&*()-_=+[]{}|;:,.<>?"; 194 195 var password = try alloc.alloc(u8, 16); 196 errdefer alloc.free(password); 197 198 init.io.random(password); 199 for (password) |*c| { 200 c.* = charset[c.* % charset.len]; 201 } 202 203 var idx_bytes: [8]u8 = undefined; 204 init.io.random(&idx_bytes); 205 const pos = std.mem.readInt(u64, idx_bytes[0..8], .little) % password.len; 206 init.io.random(&idx_bytes); 207 const idx = std.mem.readInt(u64, idx_bytes[0..8], .little) % special_chars.len; 208 password[pos] = special_chars[idx]; 209 210 return password; 211 } 212 213 fn store(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8) ![]u8 { 214 const password = try gen(init, alloc); 215 errdefer alloc.free(password); 216 217 inner(init, alloc, name, password) catch |err| { 218 std.debug.print("Failed to store password: {}\n", .{err}); 219 return err; 220 }; 221 return password; 222 } 223 224 fn inner(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8, password: []const u8) !void { 225 const pass_dir = try dir(alloc, init.environ_map); 226 defer alloc.free(pass_dir); 227 228 const full_path = try std.fs.path.join(alloc, &.{ pass_dir, name }); 229 defer alloc.free(full_path); 230 231 if (std.fs.path.dirname(full_path)) |dir_path| { 232 std.Io.Dir.createDirAbsolute(init.io, dir_path, .default_dir) catch |err| { 233 if (err != error.PathAlreadyExists) return err; 234 }; 235 } 236 237 var child = try std.process.spawn(init.io, .{ 238 .argv = &.{ "pass", "insert", "-e", name }, 239 .stdin = .pipe, 240 .stderr = .inherit, 241 }); 242 243 if (child.stdin) |stdin| { 244 var buf: [4096]u8 = undefined; 245 var fw = stdin.writer(init.io, &buf); 246 try fw.interface.writeAll(password); 247 try fw.interface.writeAll("\n"); 248 try fw.flush(); 249 stdin.close(init.io); 250 child.stdin = null; 251 } 252 253 const term = try child.wait(init.io); 254 if (term != .exited or term.exited != 0) return error.FailedExit; 255 } 256 257 fn clip(init: std.process.Init, content: []const u8) !void { 258 const args: []const []const u8 = switch (builtin.os.tag) { 259 .macos => &.{"pbcopy"}, 260 .linux => args: { 261 if (init.environ_map.get("WAYLAND_DISPLAY")) |_| { 262 break :args &.{"wl-copy"}; 263 } else break :args &.{ "xclip", "-selection", "clipboard" }; 264 }, 265 else => return, 266 }; 267 268 var child = try std.process.spawn(init.io, .{ 269 .argv = args, 270 .stdin = .pipe, 271 }); 272 273 if (child.stdin) |stdin| { 274 var buf: [4096]u8 = undefined; 275 var fw = stdin.writer(init.io, &buf); 276 try fw.interface.writeAll(content); 277 try fw.flush(); 278 stdin.close(init.io); 279 child.stdin = null; 280 } 281 282 const term = try child.wait(init.io); 283 if (term != .exited) return error.ClipboardFailed; 284 }