Source file commands.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
open Printf
open Util.Show
module Init = struct
let init ?use_sudo () =
try
let () =
print_endline
{|
Welcome to passage initial setup.
Passage will now create the default dirs for secrets and recipients keys.
A recipient identity will also be added, as well as an empty group file for root users.
The layout will be:
~/.config/passage
├── identity.key
├── keys
│ └── root.group
│ └── <user_name>.pub
└── secrets
The location of these can be overriden using environment variables. Please check `passage --help` for details.
What should be the name used for your recipient identity?|}
in
let user_name =
String.trim @@ In_channel.input_all stdin
|> ExtString.String.replace_chars (fun c ->
match c with
| ' ' -> "_"
| '\n' -> ""
| c -> Char.escaped c)
in
let () = Shell.age_generate_identity_key_root_group_exn ?use_sudo user_name in
Util.verbose_eprintlf "I: Passage setup completed.\n"
with exn ->
FileUtil.rm ~recurse:true [ Lazy.force !Config.base_dir ];
Util.die ~exn "E: Passage init failed"
end
module Get = struct
let get_secret ?use_sudo ?expected_kind ?line_number ?( = false) ?(trim_new_line = false) secret_name =
let secret_exists =
try Storage.Secrets.secret_exists secret_name with exn -> Util.die ~exn "E: %s" (show_name secret_name)
in
(match secret_exists with
| false ->
if Path.is_directory Storage.Secrets.(to_path secret_name |> Path.abs) then
Util.die "E: %s is a directory" (show_name secret_name)
else Util.die "E: no such secret: %s" (show_name secret_name)
| true -> ());
let get_line_exn secret line_number =
if line_number < 1 then Util.die "Line number should be greater than 0";
let lines = String.split_on_char '\n' secret in
match List.nth_opt lines (line_number - 1) with
| None -> Util.die "There is no secret at line %d" line_number
| Some l -> l
in
let plaintext =
try Storage.Secrets.decrypt_exn ?use_sudo secret_name
with exn -> Util.die ~exn "E: failed to decrypt %s" (show_name secret_name)
in
let secret =
match with_comments, line_number with
| true, None -> plaintext
| true, Some ln -> get_line_exn plaintext ln
| false, _ ->
let secret =
try Secret.Validation.parse_exn plaintext
with exn -> Util.die ~exn "E: failed to parse %s" (show_name secret_name)
in
let kind = secret.kind in
(match Option.is_some expected_kind && Option.get expected_kind <> kind with
| true ->
Util.die "E: %s is expected to be a %s secret but it is a %s secret" (show_name secret_name)
(Secret.kind_to_string @@ Option.get expected_kind)
(Secret.kind_to_string kind)
| false -> ());
(match line_number with
| None -> secret.text
| Some ln -> get_line_exn secret.text ln)
in
let secret =
match trim_new_line, String.ends_with ~suffix:"\n" secret with
| true, true -> String.sub secret 0 (String.length secret - 1)
| false, false -> sprintf "%s\n" secret
| true, false | false, true -> secret
in
secret
end
module List_ = struct
let list_secrets path =
let raw_path = show_path path in
let secret_exists = try Storage.Secrets.secret_exists_at path with _exn -> false in
match secret_exists with
| true -> [ Storage.Secrets.(name_of_file (Path.abs path) |> show_name) ]
| false ->
let is_dir = try Path.is_directory (Path.abs path) with exn -> Util.die ~exn "E: %s" raw_path in
(match is_dir with
| true -> Storage.(Secrets.get_secrets_tree path |> List.sort Secret_name.compare) |> List.map show_name
| false -> Util.die "No secrets at %s" raw_path)
end
module Recipients = struct
let add_recipients_if_none_exists recipients secret_path =
match Storage.Secrets.no_keys_file (Path.dirname secret_path) with
| false -> ()
| true ->
let root_recipients_names = Storage.Secrets.recipients_of_group_name_exn ~map_fn:Fun.id "@root" in
let () = Devkit.eprintfn "\nI: using recipient group @root for secret %s" (show_path secret_path) in
let recipients_names =
List.filter_map
(fun r ->
match List.mem r.Age.name root_recipients_names with
| true -> None
| false ->
let () = Printf.eprintf "I: using recipient %s for secret %s\n" r.name (show_path secret_path) in
Some r.name)
recipients
in
let () = flush stderr in
let recipients_names_with_root_group = "@root" :: (recipients_names |> List.sort String.compare) in
let recipients_file_path = Storage.Secrets.get_recipients_file_path secret_path in
let (_ : Path.t) = Path.ensure_parent recipients_file_path in
Devkit.Control.with_open_out_txt (show_path recipients_file_path) (fun oc ->
List.iter (fun line -> Printf.fprintf oc "%s\n" line) recipients_names_with_root_group)
let rewrite_recipients_file ?use_sudo secret_name new_recipients_list =
let secret_path = path_of_secret_name secret_name in
let sorted_base_recipients = Storage.Secrets.get_recipients_names secret_path in
let secret_recipients_file = Storage.Secrets.get_recipients_file_path secret_path in
let (_ : Path.t) = Path.ensure_parent secret_recipients_file in
let deduplicated_recipients = List.sort_uniq String.compare new_recipients_list in
let () =
Devkit.Control.with_open_out_txt (show_path secret_recipients_file) (fun oc ->
List.iter (fun line -> Printf.fprintf oc "%s\n" line) deduplicated_recipients)
in
let sorted_updated_recipients_names = Storage.Secrets.get_recipients_names secret_path in
if sorted_base_recipients <> sorted_updated_recipients_names then (
let secrets_affected = Storage.Secrets.get_secrets_in_folder (Path.folder_of_path secret_path) in
if secrets_affected <> [] then Storage.Secrets.refresh ?use_sudo ~force:true ~verbose:false secrets_affected
else ())
else prerr_endline "I: no changes made to the recipients"
let add_recipients_to_secret ?use_sudo secret_name recipients_to_add =
let secret_path = path_of_secret_name secret_name in
Util.Secret.check_path_exists_or_die secret_name secret_path;
Invariant.run_if_recipient ~op_string:"add recipients" ~path:secret_path ~f:(fun () ->
let () =
match Validation.validate_recipients_for_commands recipients_to_add with
| Error msg -> Util.die "%s" msg
| Ok () -> ()
in
let current_recipients = Storage.Secrets.get_recipients_names secret_path in
let new_recipients = List.sort_uniq String.compare (current_recipients @ recipients_to_add) in
match List.equal String.equal current_recipients new_recipients with
| true -> prerr_endline "I: no changes made - all specified recipients are already present"
| false ->
let () = rewrite_recipients_file ?use_sudo secret_name new_recipients in
let added_count = List.length new_recipients - List.length current_recipients in
Devkit.eprintfn "I: added %d recipient%s" added_count (if added_count = 1 then "" else "s"))
let remove_recipients_from_secret ?use_sudo secret_name recipients_to_remove =
let secret_path = path_of_secret_name secret_name in
Util.Secret.check_path_exists_or_die secret_name secret_path;
Invariant.run_if_recipient ~op_string:"remove recipients" ~path:secret_path ~f:(fun () ->
let current_recipients = Storage.Secrets.get_recipients_names secret_path in
let new_recipients = List.filter (fun r -> not (List.mem r recipients_to_remove)) current_recipients in
let non_existent = List.filter (fun r -> not (List.mem r current_recipients)) recipients_to_remove in
if new_recipients = [] then Util.die "E: cannot remove all recipients - at least one recipient must remain"
else if current_recipients = new_recipients then (
match non_existent with
| [] -> prerr_endline "I: no changes made - specified recipients were already absent"
| _ -> Util.die "E: recipients not found: %s" (String.concat ", " non_existent))
else (
let () =
if non_existent <> [] then
Devkit.eprintfn "W: recipients not found to remove: %s" (String.concat ", " non_existent)
else ()
in
let () = rewrite_recipients_file ?use_sudo secret_name new_recipients in
let removed_count = List.length current_recipients - List.length new_recipients in
Devkit.eprintfn "I: removed %d recipient%s" removed_count (if removed_count = 1 then "" else "s")))
let list_recipient_secrets ?use_sudo ?(verbose = false) recipients_names =
if recipients_names = [] then Util.die "E: Must specify at least one recipient name";
let number_of_recipients = List.length recipients_names in
let all_recipient_names = Storage.Keys.all_recipient_names () in
List.iter
(fun recipient_name ->
let open Storage in
match List.mem recipient_name all_recipient_names, Age.is_group_recipient recipient_name with
| false, false -> Devkit.eprintfn "E: no such recipient %s" recipient_name
| _ ->
match Secrets.get_secrets_for_recipient recipient_name with
| [] -> Devkit.eprintfn "\nNo secrets found for %s" recipient_name
| secrets ->
let () =
if number_of_recipients > 1 then Devkit.eprintfn "\nSecrets which %s is a recipient of:" recipient_name
else ()
in
let sorted = List.sort Secret_name.compare secrets in
let print_secret secret =
match verbose with
| false -> Printf.printf "%s\n" (show_name secret)
| true ->
try
let plaintext = Util.Secret.decrypt_silently ?use_sudo secret in
Printf.printf "%s\n" (Secret.Validation.validity_to_string (show_name secret) plaintext)
with _ -> Printf.printf "🚨 %s [ WARNING: failed to decrypt ]\n" (show_name secret)
in
let () = List.iter print_secret sorted in
flush stderr)
recipients_names
let list_recipients path expand_groups =
let string_path = show_path path in
let print_from_recipient_list recipients =
List.iter
(fun (r : Age.recipient) ->
(match r.keys with
| [] -> Devkit.eprintfn "W: no keys found for %s" r.name
| _ -> ());
print_endline r.name)
recipients
in
match Age.is_group_recipient string_path with
| true ->
Storage.Secrets.recipients_of_group_name_exn ~map_fn:Storage.Secrets.recipient_of_name string_path
|> print_from_recipient_list
| false ->
match Storage.Secrets.secret_exists_at path || Storage.Secrets.get_secrets_tree path <> [] with
| false -> Util.die "E: no such secret %s" (show_path path)
| true ->
match expand_groups with
| true ->
(match Storage.Secrets.get_recipients_from_path_exn path with
| exception exn -> Util.Secret.die_failed_get_recipients ~exn ""
| [] -> Util.Secret.die_no_recipients_found path
| recipients -> print_from_recipient_list recipients)
| false ->
match Storage.Secrets.get_recipients_names path with
| [] -> Util.Secret.die_no_recipients_found path
| recipients_names ->
List.iter
(fun recipient_name ->
let recipient_keys =
match Age.is_group_recipient recipient_name with
| false -> Storage.Keys.keys_of_recipient recipient_name
| true ->
try
let (_ : Age.recipient list) =
Storage.Secrets.recipients_of_group_name_exn ~map_fn:Storage.Secrets.recipient_of_name recipient_name
in
[ Age.Key.inject "" ]
with exn ->
Devkit.printfn "E: couldn't retrieve recipients for group %s. Reason: %s" recipient_name
(Printexc.to_string exn);
[]
in
(match recipient_keys with
| [] -> Devkit.eprintfn "W: no keys found for %s" recipient_name
| _ -> ());
print_endline recipient_name)
recipients_names
end
module Refresh = struct
let refresh_secrets ?use_sudo ?(verbose = false) paths =
let secrets =
List.fold_left
(fun acc path_or_recip ->
(match Age.is_group_recipient path_or_recip with
| true ->
let recipient =
let r = Devkit.Stre.drop_prefix path_or_recip "@" in
match List.mem r (Storage.Secrets.all_groups_names ()), r = "everyone" with
| false, false -> r
| _ -> path_or_recip
in
(match Storage.Secrets.get_secrets_for_recipient recipient with
| [] -> Util.die "E: no secrets found for recipient %s" path_or_recip
| secrets -> secrets)
| false ->
let path =
try Path.build_rel_path path_or_recip
with Failure s -> Util.die "E: invalid path %s: %s" path_or_recip s
in
(match Storage.Secrets.get_secrets_tree path with
| _ :: _ as secrets -> secrets
| [] ->
match Storage.Secrets.secret_exists_at path with
| true -> [ secret_name_of_path path ]
| false -> Util.die "E: no secrets at %s" path_or_recip))
@ acc)
[] paths
in
let secrets = List.sort_uniq Storage.Secret_name.compare secrets in
Storage.Secrets.refresh ?use_sudo ~verbose secrets
end
module Template = struct
open Template
let substitute ~template =
let substituted_ast = List.map substitute_iden template in
build_text_from_ast substituted_ast
let substitute_file ~template_file =
let template = parse_file template_file in
substitute ~template
let list_template_secrets template_file =
let tree = try parse_file template_file with exn -> Util.die ~exn "Failed to parse the file" in
List.filter_map
(fun node ->
match node with
| Template_ast.Text _ -> None
| Template_ast.Iden secret_name -> Some secret_name)
tree
|> List.sort_uniq String.compare
end
module Realpath = struct
let realpath paths =
paths
|> List.map (fun path ->
let abs_path = Path.abs path in
if Storage.Secrets.secret_exists_at path then (
let secret_name = secret_name_of_path path in
Ok (show_path (Path.abs (Storage.Secrets.agefile_of_name secret_name))))
else if Path.is_directory abs_path then (
let str = show_path abs_path in
if Path.is_dot (Path.build_rel_path (show_path path)) then Ok (Storage.Secrets.get_secrets_dir () ^ "/")
else Ok (str ^ if String.ends_with ~suffix:"/" str then "" else "/"))
else Error (sprintf "real path of secret/folder %S not found" (show_path path)))
end
module Rm = struct
let rm_secrets ~verbose ~paths ~force ?confirm () =
List.iter
(fun path ->
let is_directory = Path.is_directory (Path.abs path) in
(match Storage.Secrets.secret_exists_at path, is_directory with
| false, false -> Util.die "E: no secrets exist at %s" (show_path path)
| _ -> ());
let string_path = show_path path in
let r =
match (not (Unix.isatty Unix.stdin)) || force with
| true -> Storage.Secrets.rm ~is_directory path
| false ->
match confirm with
| Some f ->
(match f ~path with
| true -> Storage.Secrets.rm ~is_directory path
| false -> Storage.Secrets.Skipped)
| None -> Util.die "E: please provide a confirmation function for TTY environments"
in
match r with
| Storage.Secrets.Succeeded () -> Util.verbose_eprintlf ~verbose "I: removed %s" string_path
| Skipped -> Devkit.eprintfn "I: skipped deleting %s" string_path
| Failed exn -> Util.die "E: failed to delete %s : %s" string_path (Devkit.Exn.to_string exn))
paths
end
module Search = struct
let search_secrets ?(verbose = false) ?use_sudo pattern path =
let secrets = Storage.Secrets.get_secrets_tree path |> List.sort Storage.Secret_name.compare in
let n_skipped, n_failed, n_matched, matched_secrets =
List.fold_left
(fun (n_skipped, n_failed, n_matched, matched_secrets) secret ->
match Storage.Secrets.search ?use_sudo secret pattern with
| Succeeded true -> n_skipped, n_failed, n_matched + 1, secret :: matched_secrets
| Succeeded false -> n_skipped, n_failed, n_matched, matched_secrets
| Skipped ->
Util.verbose_eprintlf ~verbose "I: skipped %s" (show_name secret);
n_skipped + 1, n_failed, n_matched, matched_secrets
| Failed exn ->
Devkit.eprintfn "W: failed to search %s : %s" (show_name secret) (Devkit.Exn.to_string exn);
n_skipped, n_failed + 1, n_matched, matched_secrets)
(0, 0, 0, []) secrets
in
List.rev matched_secrets |> List.iter (fun s -> print_endline (show_name s));
Printf.eprintf "I: skipped %d secrets, failed to search %d secrets and matched %d secrets\n" n_skipped n_failed
n_matched
end
module Show = struct
let list_secrets_tree path =
let full_path = Path.abs path in
match Path.is_directory full_path, Storage.Secrets.secret_exists_at path with
| false, false -> Util.die "No secrets at this path : %s" (show_path full_path)
| false, true -> Get.get_secret ~with_comments:true (secret_name_of_path path)
| true, _ ->
let tree = Dirtree.of_path (Path.to_fpath (Path.abs path)) in
Dirtree.pp tree
end
module Edit = struct
let new_secret_recipients_notice =
{|
If you are adding a new secret in a new folder, please keep recipients to a minimum and include the following:
- @root
- yourself
- people who help manage the secret, or people who would have access to it anyway
- people who need access to do their job
- servers/clusters that will consume the secret
If the secret is a staging secret, its only recipient should be @everyone.
|}
let show_recipients_notice_if_true cond = if cond then prerr_endline new_secret_recipients_notice
let edit_secret ?use_sudo ?(self_fallback = false) ?(verbose = false) ?allow_retry ~get_updated_secret secret_name =
let secret_name_str = show_name secret_name in
let original_secret =
match Storage.Secrets.secret_exists secret_name with
| false -> None
| true ->
try Devkit.some @@ Storage.Secrets.decrypt_exn ?use_sudo secret_name
with exn -> Util.die ~exn "E: failed to decrypt %s" secret_name_str
in
try
let updated_secret = Result.bind (get_updated_secret original_secret) Validation.validate_secret in
match updated_secret, original_secret with
| Error e, _ -> Util.die "E: %s" e
| Ok updated_secret, Some original_secret when updated_secret = original_secret ->
prerr_endline "I: secret unchanged"
| Ok updated_secret, _ ->
let secret_path = path_of_secret_name secret_name in
let secret_recipients' = Storage.Secrets.get_recipients_from_path_exn secret_path in
let secret_recipients =
if secret_recipients' = [] && self_fallback then (
let own_recipients = Storage.Secrets.recipients_of_own_id ?use_sudo:None () in
let () = Recipients.add_recipients_if_none_exists own_recipients secret_path in
Storage.Secrets.get_recipients_from_path_exn secret_path)
else secret_recipients'
in
if secret_recipients = [] then Util.die "E: no recipients specified for this secret"
else (
let is_first_secret_in_new_folder = Option.is_none original_secret && secret_recipients' = [] in
match allow_retry with
| Some encrypt_with_retry ->
show_recipients_notice_if_true is_first_secret_in_new_folder;
encrypt_with_retry ~plaintext:updated_secret ~secret_name secret_recipients
| None ->
try
show_recipients_notice_if_true is_first_secret_in_new_folder;
Storage.Secrets.encrypt_exn ?use_sudo ~verbose ~plaintext:updated_secret ~secret_name secret_recipients
with exn -> Util.die ~exn "E: encrypting %s failed" secret_name_str)
with Failure s -> Util.die "%s" s
end
module Create = struct
let base_check ?use_sudo secret_name =
match Storage.Secrets.secret_exists secret_name with
| true -> Util.die "E: refusing to create: a secret by that name already exists"
| false ->
let path = Storage.Secrets.(to_path secret_name) in
Invariant.die_if_invariant_fails ?use_sudo ~op_string:"create" path
let add ?use_sudo ~ secret_name secret_text =
base_check ?use_sudo secret_name;
Edit.edit_secret ?use_sudo secret_name ~self_fallback:true ~get_updated_secret:(fun _ ->
let parsed_secret = Secret.Validation.parse_exn secret_text in
let =
match comments, parsed_secret.comments with
| Some , None | None, Some -> Some comment
| None, None -> None
| Some _, Some _ ->
Util.die
"E: secret text already contains comments. Either use the secret text with comments or use the --comment \
flag."
in
match Validation.validate_comments (Option.value ~default:"" comments) with
| Error e -> Util.die "E: invalid comment format: %s" e
| Ok -> Ok (Util.Secret.reconstruct_secret ~comments parsed_secret))
let bare ?use_sudo ~f secret_name =
base_check ?use_sudo secret_name;
f secret_name
end
module Replace = struct
let replace_secret secret_name new_secret_plaintext =
let recipients = Util.Recipients.get_recipients_or_die secret_name in
Invariant.run_if_recipient ~op_string:"replace secret"
~path:Storage.Secrets.(to_path secret_name)
~f:(fun () ->
if new_secret_plaintext = "" then Util.die "E: invalid input, empty secrets are not allowed.";
let is_singleline_secret =
match String.split_on_char '\n' new_secret_plaintext with
| [ _ ] | [ _; "" ] -> true
| _ -> false
in
let updated_secret =
match Storage.Secrets.secret_exists secret_name with
| false ->
(match is_singleline_secret with
| true -> new_secret_plaintext
| false -> "\n\n" ^ new_secret_plaintext)
| true ->
let original_secret =
try
let secret_plaintext = Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name in
Secret.Validation.parse_exn secret_plaintext
with _e ->
Util.die
"E: unable to parse secret %s's format. If we proceed, the comments will be lost. Aborting. Please \
use the edit command to replace and fix this secret."
(show_name secret_name)
in
Util.Secret.reconstruct_with_new_text ~is_singleline:is_singleline_secret ~new_text:new_secret_plaintext
~existing_comments:original_secret.comments
in
try Storage.Secrets.encrypt_exn ~verbose:false ~plaintext:updated_secret ~secret_name recipients
with exn -> Util.die ~exn "E: encrypting %s failed" (show_name secret_name))
let ?use_sudo secret_name =
let secret_name_str = show_name secret_name in
let recipients = Util.Recipients.get_recipients_or_die secret_name in
Invariant.run_if_recipient ~op_string:"replace comments"
~path:Storage.Secrets.(to_path secret_name)
~f:(fun () ->
match Storage.Secrets.secret_exists secret_name with
| false -> Util.die "E: no such secret: %s" secret_name_str
| true ->
let original_secret =
try Util.Secret.decrypt_and_parse ?use_sudo ~silence_stderr:true secret_name
with _e ->
Util.die
"E: unable to parse secret %s's format. Please fix it before replacing the comments,or use the edit \
command"
secret_name_str
in
let = get_new_comments original_secret.comments in
let updated_secret = Util.Secret.reconstruct_secret ~comments:new_comments original_secret in
(try Storage.Secrets.encrypt_exn ?use_sudo ~verbose:false ~plaintext:updated_secret ~secret_name recipients
with exn -> Util.die ~exn "E: encrypting %s failed" secret_name_str))
end