uag-envsec 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uag_envsec/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ """uag_envsec
2
+
3
+ Standalone helpers for encrypting/decrypting environment files and values.
4
+
5
+ This package is intentionally small and can be reused outside uagent.
6
+ """
7
+
8
+ from .secret_core import (
9
+ CURRENT_FORMAT_VERSION,
10
+ DEFAULT_KEY_FILENAME,
11
+ DEFAULT_SEC_SUFFIX,
12
+ KEYRING_SERVICE,
13
+ KEYRING_USERNAME,
14
+ LEGACY_FORMAT_VERSION,
15
+ UnsupportedCiphertextVersionError,
16
+ decrypt_file,
17
+ decrypt_text,
18
+ detect_format_version,
19
+ encrypt_file,
20
+ encrypt_text,
21
+ ensure_key_file,
22
+ inspect_file,
23
+ load_key,
24
+ migrate_file,
25
+ migrate_key_file_to_keyring,
26
+ migrate_text,
27
+ rekey_file,
28
+ rekey_text,
29
+ save_key,
30
+ )
31
+
32
+ __all__ = [
33
+ "CURRENT_FORMAT_VERSION",
34
+ "DEFAULT_KEY_FILENAME",
35
+ "DEFAULT_SEC_SUFFIX",
36
+ "KEYRING_SERVICE",
37
+ "KEYRING_USERNAME",
38
+ "LEGACY_FORMAT_VERSION",
39
+ "UnsupportedCiphertextVersionError",
40
+ "decrypt_file",
41
+ "decrypt_text",
42
+ "detect_format_version",
43
+ "encrypt_file",
44
+ "encrypt_text",
45
+ "ensure_key_file",
46
+ "inspect_file",
47
+ "load_key",
48
+ "migrate_file",
49
+ "migrate_key_file_to_keyring",
50
+ "migrate_text",
51
+ "rekey_file",
52
+ "rekey_text",
53
+ "save_key",
54
+ ]
uag_envsec/cli.py ADDED
@@ -0,0 +1,641 @@
1
+ """CLI for encrypting and updating .env.sec files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import getpass
7
+ import os
8
+ import re
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ from .secret_core import (
14
+ CURRENT_FORMAT_VERSION,
15
+ DEFAULT_SEC_SUFFIX,
16
+ LEGACY_FORMAT_VERSION,
17
+ decrypt_file,
18
+ decrypt_text,
19
+ detect_format_version,
20
+ default_key_path,
21
+ encrypt_text,
22
+ encrypt_file,
23
+ ensure_key_file,
24
+ inspect_file,
25
+ load_key,
26
+ migrate_file,
27
+ rekey_file,
28
+ )
29
+
30
+ _ENV_KEY_RE = re.compile(
31
+ r"^\s*(?:export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=", re.MULTILINE
32
+ )
33
+
34
+
35
+ def _build_encrypt_parser() -> argparse.ArgumentParser:
36
+ parser = argparse.ArgumentParser(
37
+ prog="uag_envsec",
38
+ description="Encrypt a .env file into .env.sec.",
39
+ )
40
+ parser.add_argument(
41
+ "path",
42
+ nargs="?",
43
+ default=".env",
44
+ help="Path to the input .env file (default: .env)",
45
+ )
46
+ parser.add_argument(
47
+ "--output",
48
+ default=None,
49
+ help="Output path for the encrypted file (default: input path + .sec)",
50
+ )
51
+ parser.add_argument(
52
+ "--key-file",
53
+ default=None,
54
+ help="Key file path (default: ~/.uag/uag_envsec_key)",
55
+ )
56
+ parser.add_argument(
57
+ "--force",
58
+ action="store_true",
59
+ help="Overwrite the output file if it already exists",
60
+ )
61
+ parser.add_argument(
62
+ "--format-version",
63
+ type=int,
64
+ choices=[LEGACY_FORMAT_VERSION, CURRENT_FORMAT_VERSION],
65
+ default=CURRENT_FORMAT_VERSION,
66
+ help=(
67
+ "Ciphertext envelope version (default: 1; use 0 for older-uag compatibility)"
68
+ ),
69
+ )
70
+ return parser
71
+
72
+
73
+ def _build_decrypt_parser() -> argparse.ArgumentParser:
74
+ parser = argparse.ArgumentParser(
75
+ prog="uag_envsec decrypt",
76
+ description="Decrypt an encrypted env file to a private output file.",
77
+ )
78
+ parser.add_argument("--file", required=True, help="Encrypted input file")
79
+ parser.add_argument(
80
+ "--output",
81
+ default=None,
82
+ help="Plaintext output path (default: append .decrypted to the input filename)",
83
+ )
84
+ parser.add_argument(
85
+ "--key-file",
86
+ default=None,
87
+ help="Key file path (default: configured key backend)",
88
+ )
89
+ parser.add_argument(
90
+ "--force",
91
+ action="store_true",
92
+ help="Overwrite an existing output file (the encrypted source is never overwritten)",
93
+ )
94
+ return parser
95
+
96
+
97
+ def _build_inspect_parser() -> argparse.ArgumentParser:
98
+ parser = argparse.ArgumentParser(
99
+ prog="uag_envsec inspect",
100
+ description="Inspect ciphertext version and verify that it can be decrypted.",
101
+ )
102
+ parser.add_argument("--file", required=True, help="Encrypted input file")
103
+ parser.add_argument(
104
+ "--key-file",
105
+ default=None,
106
+ help="Key file path (default: configured key backend)",
107
+ )
108
+ return parser
109
+
110
+
111
+ def _build_add_parser() -> argparse.ArgumentParser:
112
+ parser = argparse.ArgumentParser(
113
+ prog="uag_envsec add",
114
+ description="Add or update a variable inside an encrypted .env.sec file.",
115
+ )
116
+ parser.add_argument(
117
+ "--file",
118
+ default=".env.sec",
119
+ help="Path to the encrypted .env.sec file (default: .env.sec)",
120
+ )
121
+ parser.add_argument(
122
+ "--output",
123
+ default=None,
124
+ help="Output path (default: overwrite the input file)",
125
+ )
126
+ parser.add_argument(
127
+ "--key-file",
128
+ default=None,
129
+ help="Key file path (default: ~/.uag/uag_envsec_key)",
130
+ )
131
+ parser.add_argument(
132
+ "--key",
133
+ default=None,
134
+ help="Environment variable name",
135
+ )
136
+ parser.add_argument(
137
+ "--value",
138
+ default=None,
139
+ help="Environment variable value",
140
+ )
141
+ parser.add_argument(
142
+ "--force",
143
+ action="store_true",
144
+ help="Overwrite the output file if it already exists",
145
+ )
146
+ parser.add_argument(
147
+ "--format-version",
148
+ type=int,
149
+ choices=[LEGACY_FORMAT_VERSION, CURRENT_FORMAT_VERSION],
150
+ default=CURRENT_FORMAT_VERSION,
151
+ help="Output ciphertext version (default: 1; use 0 for older uag)",
152
+ )
153
+ return parser
154
+
155
+
156
+ def _build_delete_parser() -> argparse.ArgumentParser:
157
+ parser = argparse.ArgumentParser(
158
+ prog="uag_envsec delete",
159
+ description="Delete a variable from an encrypted .env.sec file.",
160
+ )
161
+ parser.add_argument(
162
+ "--file",
163
+ default=".env.sec",
164
+ help="Path to the encrypted .env.sec file (default: .env.sec)",
165
+ )
166
+ parser.add_argument(
167
+ "--output",
168
+ default=None,
169
+ help="Output path (default: overwrite the input file)",
170
+ )
171
+ parser.add_argument(
172
+ "--key-file",
173
+ default=None,
174
+ help="Key file path (default: ~/.uag/uag_envsec_key)",
175
+ )
176
+ parser.add_argument(
177
+ "--key",
178
+ required=True,
179
+ help="Environment variable name to delete",
180
+ )
181
+ parser.add_argument(
182
+ "--force",
183
+ action="store_true",
184
+ help="Overwrite the output file if it already exists",
185
+ )
186
+ parser.add_argument(
187
+ "--format-version",
188
+ type=int,
189
+ choices=[LEGACY_FORMAT_VERSION, CURRENT_FORMAT_VERSION],
190
+ default=CURRENT_FORMAT_VERSION,
191
+ help="Output ciphertext version (default: 1; use 0 for older uag)",
192
+ )
193
+ return parser
194
+
195
+
196
+ def _build_migrate_parser() -> argparse.ArgumentParser:
197
+ parser = argparse.ArgumentParser(
198
+ prog="uag_envsec migrate",
199
+ description="Migrate an encrypted env file to another format version.",
200
+ )
201
+ parser.add_argument(
202
+ "--file",
203
+ required=True,
204
+ help="Encrypted input file",
205
+ )
206
+ parser.add_argument(
207
+ "--to",
208
+ "--format-version",
209
+ dest="target_version",
210
+ type=int,
211
+ choices=[LEGACY_FORMAT_VERSION, CURRENT_FORMAT_VERSION],
212
+ default=CURRENT_FORMAT_VERSION,
213
+ help="Target ciphertext format version (default: 1)",
214
+ )
215
+ parser.add_argument(
216
+ "--output",
217
+ default=None,
218
+ help="Output path (default: append .v<version> to the input filename)",
219
+ )
220
+ parser.add_argument(
221
+ "--key-file",
222
+ default=None,
223
+ help="Key file path (default: configured key backend)",
224
+ )
225
+ parser.add_argument(
226
+ "--force",
227
+ action="store_true",
228
+ help="Overwrite an existing output file (the source is never overwritten)",
229
+ )
230
+ return parser
231
+
232
+
233
+ def _build_rekey_parser() -> argparse.ArgumentParser:
234
+ parser = argparse.ArgumentParser(
235
+ prog="uag_envsec rekey",
236
+ description="Re-encrypt an encrypted file using a different key.",
237
+ )
238
+ parser.add_argument("--file", required=True, help="Encrypted input file")
239
+ parser.add_argument(
240
+ "--new-key-file",
241
+ required=True,
242
+ help="Key file to use for the new ciphertext; created if missing",
243
+ )
244
+ parser.add_argument(
245
+ "--old-key-file",
246
+ default=None,
247
+ help="Current key file (default: configured key backend)",
248
+ )
249
+ parser.add_argument(
250
+ "--output",
251
+ default=None,
252
+ help="Output path (default: append .rekeyed to the input filename)",
253
+ )
254
+ parser.add_argument(
255
+ "--format-version",
256
+ type=int,
257
+ choices=[LEGACY_FORMAT_VERSION, CURRENT_FORMAT_VERSION],
258
+ default=CURRENT_FORMAT_VERSION,
259
+ help="Output envelope version (default: 1; use 0 for older uag)",
260
+ )
261
+ parser.add_argument(
262
+ "--force",
263
+ action="store_true",
264
+ help="Overwrite an existing output file (the source is never overwritten)",
265
+ )
266
+ return parser
267
+
268
+
269
+ def _format_env_value(value: str) -> str:
270
+ if value == "":
271
+ return '""'
272
+ if "\n" in value or "\r" in value:
273
+ escaped = (
274
+ value.replace("\\", "\\\\")
275
+ .replace('"', '\\"')
276
+ .replace("\n", "\\n")
277
+ .replace("\r", "\\r")
278
+ )
279
+ return f'"{escaped}"'
280
+ if re.fullmatch(r"[A-Za-z0-9_./:@%+=,-]+", value):
281
+ return value
282
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
283
+ return f'"{escaped}"'
284
+
285
+
286
+ def _upsert_env_text(text: str, key: str, value: str) -> str:
287
+ line = f"{key}={_format_env_value(value)}"
288
+ lines = text.splitlines()
289
+ for idx, current in enumerate(lines):
290
+ match = _ENV_KEY_RE.match(current)
291
+ if match and match.group("key") == key:
292
+ lines[idx] = line
293
+ break
294
+ else:
295
+ lines.append(line)
296
+
297
+ updated = "\n".join(lines)
298
+ if text.endswith(("\n", "\r")):
299
+ return updated + "\n"
300
+ return updated
301
+
302
+
303
+ def _read_key_value_from_args(args: argparse.Namespace) -> tuple[str, str]:
304
+ if args.key is not None and args.value is not None:
305
+ key = args.key.strip()
306
+ value = args.value
307
+ return key, value
308
+
309
+ if args.key is not None and args.value is None:
310
+ key = args.key.strip()
311
+ value = getpass.getpass("Value: ")
312
+ return key, value
313
+
314
+ if args.key is None and args.value is not None:
315
+ raise SystemExit("--value requires --key")
316
+
317
+ key = input("Key: ").strip()
318
+ if not key:
319
+ raise SystemExit("Key is required")
320
+ value = getpass.getpass("Value: ")
321
+ return key, value
322
+
323
+
324
+ def _resolve_key_file(raw: str | None) -> Path:
325
+ return Path(raw) if raw else default_key_path()
326
+
327
+
328
+ def _write_text_atomic(
329
+ path: Path,
330
+ content: str,
331
+ *,
332
+ overwrite: bool = True,
333
+ ) -> None:
334
+ """Write encrypted text through a private, unique file in the target dir."""
335
+ path.parent.mkdir(parents=True, exist_ok=True)
336
+ fd, temporary_name = tempfile.mkstemp(
337
+ prefix=f".{path.name}.",
338
+ suffix=".tmp",
339
+ dir=str(path.parent),
340
+ )
341
+ temporary_path = Path(temporary_name)
342
+ try:
343
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as stream:
344
+ stream.write(content)
345
+ stream.flush()
346
+ os.fsync(stream.fileno())
347
+ if overwrite:
348
+ os.replace(temporary_path, path)
349
+ else:
350
+ try:
351
+ os.link(temporary_path, path)
352
+ except FileExistsError as exc:
353
+ raise FileExistsError(f"Output file already exists: {path}") from exc
354
+ else:
355
+ temporary_path.unlink()
356
+ finally:
357
+ try:
358
+ temporary_path.unlink()
359
+ except FileNotFoundError:
360
+ pass
361
+
362
+
363
+ def _run_encrypt(args: argparse.Namespace) -> int:
364
+ src_path = Path(args.path)
365
+ if not src_path.is_file():
366
+ print(f"Input file not found: {src_path}", file=sys.stderr)
367
+ return 1
368
+
369
+ out_path = (
370
+ Path(args.output) if args.output else Path(str(src_path) + DEFAULT_SEC_SUFFIX)
371
+ )
372
+ if out_path.exists() and not args.force:
373
+ print(f"Output file already exists: {out_path} (use --force)", file=sys.stderr)
374
+ return 1
375
+
376
+ key_file = _resolve_key_file(args.key_file) if args.key_file else None
377
+ try:
378
+ ensure_key_file(key_file)
379
+ result_path = encrypt_file(
380
+ src_path,
381
+ key_path=key_file,
382
+ output_path=out_path,
383
+ format_version=args.format_version,
384
+ overwrite=args.force,
385
+ )
386
+ except Exception as exc:
387
+ print(f"Failed to encrypt {src_path}: {exc}", file=sys.stderr)
388
+ return 1
389
+ print(str(result_path))
390
+ return 0
391
+
392
+
393
+ def _run_decrypt(args: argparse.Namespace) -> int:
394
+ source_path = Path(args.file)
395
+ if not source_path.is_file():
396
+ print(f"Encrypted input file not found: {source_path}", file=sys.stderr)
397
+ return 1
398
+
399
+ destination = (
400
+ Path(args.output)
401
+ if args.output
402
+ else Path(f"{source_path}.decrypted")
403
+ )
404
+ if source_path.resolve() == destination.resolve():
405
+ print("Decrypted output must differ from the encrypted source", file=sys.stderr)
406
+ return 1
407
+ if destination.exists() and not args.force:
408
+ print(f"Decrypted output already exists: {destination}", file=sys.stderr)
409
+ return 1
410
+
411
+ key_file = Path(args.key_file) if args.key_file else None
412
+ try:
413
+ result_path = decrypt_file(
414
+ source_path,
415
+ key_path=key_file,
416
+ output_path=destination,
417
+ overwrite=args.force,
418
+ )
419
+ except Exception as exc:
420
+ print(f"Failed to decrypt {source_path}: {exc}", file=sys.stderr)
421
+ return 1
422
+
423
+ print(str(result_path))
424
+ return 0
425
+
426
+
427
+ def _run_inspect(args: argparse.Namespace) -> int:
428
+ source_path = Path(args.file)
429
+ key_file = Path(args.key_file) if args.key_file else None
430
+ try:
431
+ result = inspect_file(source_path, key_path=key_file)
432
+ except Exception as exc:
433
+ print(f"Inspection failed: {exc}", file=sys.stderr)
434
+ return 1
435
+
436
+ print(f"format_version: {result['format_version']}")
437
+ print(f"ciphertext_bytes: {result['ciphertext_bytes']}")
438
+ print(f"authenticated: {'yes' if result['authenticated'] else 'no'}")
439
+ print(f"decryptable: {'yes' if result['decryptable'] else 'no'}")
440
+ return 0
441
+
442
+
443
+ def _run_add(args: argparse.Namespace) -> int:
444
+ enc_path = Path(args.file)
445
+ if not enc_path.exists():
446
+ print(f"Encrypted file not found: {enc_path}", file=sys.stderr)
447
+ return 1
448
+
449
+ out_path = Path(args.output) if args.output else enc_path
450
+ if (
451
+ out_path.exists()
452
+ and out_path.resolve() != enc_path.resolve()
453
+ and not args.force
454
+ ):
455
+ print(f"Output file already exists: {out_path} (use --force)", file=sys.stderr)
456
+ return 1
457
+
458
+ key_file = _resolve_key_file(args.key_file) if args.key_file else None
459
+ if key_file is not None and not key_file.exists():
460
+ print(f"Key file not found: {key_file}", file=sys.stderr)
461
+ return 1
462
+ ensure_key_file(key_file)
463
+
464
+ key, value = _read_key_value_from_args(args)
465
+ if not key:
466
+ print("Key is required", file=sys.stderr)
467
+ return 1
468
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
469
+ print(f"Invalid environment variable name: {key}", file=sys.stderr)
470
+ return 1
471
+
472
+ try:
473
+ encrypted_input = enc_path.read_text(encoding="utf-8").strip()
474
+ detect_format_version(encrypted_input)
475
+ plaintext = decrypt_text(encrypted_input, key_path=key_file)
476
+ except Exception as exc:
477
+ print(f"Failed to decrypt {enc_path}: {exc}", file=sys.stderr)
478
+ return 1
479
+
480
+ updated = _upsert_env_text(plaintext, key, value)
481
+ _write_text_atomic(
482
+ out_path,
483
+ encrypt_text(updated, key_path=key_file, format_version=args.format_version),
484
+ overwrite=args.force or out_path.resolve() == enc_path.resolve(),
485
+ )
486
+ print(str(out_path))
487
+ return 0
488
+
489
+
490
+ def _run_delete(args: argparse.Namespace) -> int:
491
+ enc_path = Path(args.file)
492
+ if not enc_path.exists():
493
+ print(f"Encrypted file not found: {enc_path}", file=sys.stderr)
494
+ return 1
495
+
496
+ out_path = Path(args.output) if args.output else enc_path
497
+ if (
498
+ out_path.exists()
499
+ and out_path.resolve() != enc_path.resolve()
500
+ and not args.force
501
+ ):
502
+ print(f"Output file already exists: {out_path} (use --force)", file=sys.stderr)
503
+ return 1
504
+
505
+ key = args.key.strip()
506
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
507
+ print(f"Invalid environment variable name: {key}", file=sys.stderr)
508
+ return 1
509
+
510
+ key_file = _resolve_key_file(args.key_file) if args.key_file else None
511
+ if key_file is not None and not key_file.exists():
512
+ print(f"Key file not found: {key_file}", file=sys.stderr)
513
+ return 1
514
+ ensure_key_file(key_file)
515
+
516
+ try:
517
+ encrypted_input = enc_path.read_text(encoding="utf-8").strip()
518
+ detect_format_version(encrypted_input)
519
+ plaintext = decrypt_text(encrypted_input, key_path=key_file)
520
+ except Exception as exc:
521
+ print(f"Failed to decrypt {enc_path}: {exc}", file=sys.stderr)
522
+ return 1
523
+
524
+ pattern = re.compile(
525
+ rf"^[ \t]*(?:export[ \t]+)?{re.escape(key)}[ \t]*=[^\r\n]*(?:\r\n|\n|\r|$)",
526
+ re.MULTILINE,
527
+ )
528
+ updated, count = pattern.subn("", plaintext)
529
+ if count == 0:
530
+ print(f"Environment variable not found: {key}", file=sys.stderr)
531
+ return 1
532
+
533
+ try:
534
+ _write_text_atomic(
535
+ out_path,
536
+ encrypt_text(updated, key_path=key_file, format_version=args.format_version),
537
+ overwrite=args.force or out_path.resolve() == enc_path.resolve(),
538
+ )
539
+ except Exception as exc:
540
+ print(f"Failed to encrypt {out_path}: {exc}", file=sys.stderr)
541
+ return 1
542
+
543
+ print(str(out_path))
544
+ return 0
545
+
546
+
547
+ def _run_migrate(args: argparse.Namespace) -> int:
548
+ source_path = Path(args.file)
549
+ if not source_path.is_file():
550
+ print(f"Encrypted input file not found: {source_path}", file=sys.stderr)
551
+ return 1
552
+
553
+ key_file = Path(args.key_file) if args.key_file else None
554
+ try:
555
+ result_path = migrate_file(
556
+ source_path,
557
+ target_version=args.target_version,
558
+ key_path=key_file,
559
+ output_path=args.output,
560
+ overwrite=args.force,
561
+ )
562
+ except Exception as exc:
563
+ print(f"Failed to migrate {source_path}: {exc}", file=sys.stderr)
564
+ return 1
565
+
566
+ if result_path.resolve() == source_path.resolve() and args.output is None:
567
+ print(
568
+ f"{source_path} is already format version {args.target_version}; "
569
+ "no migration needed."
570
+ )
571
+ else:
572
+ print(str(result_path))
573
+ return 0
574
+
575
+
576
+ def _run_rekey(args: argparse.Namespace) -> int:
577
+ source_path = Path(args.file)
578
+ if not source_path.is_file():
579
+ print(f"Encrypted input file not found: {source_path}", file=sys.stderr)
580
+ return 1
581
+
582
+ destination = (
583
+ Path(args.output) if args.output else Path(f"{source_path}.rekeyed")
584
+ )
585
+ if source_path.resolve() == destination.resolve():
586
+ print("Rekey output must differ from the source file", file=sys.stderr)
587
+ return 1
588
+ if destination.exists() and not args.force:
589
+ print(f"Rekey output already exists: {destination}", file=sys.stderr)
590
+ return 1
591
+
592
+ old_key_path = Path(args.old_key_file) if args.old_key_file else None
593
+ new_key_path = Path(args.new_key_file)
594
+ try:
595
+ load_key(old_key_path)
596
+ ensure_key_file(new_key_path)
597
+ result_path = rekey_file(
598
+ source_path,
599
+ new_key_path=new_key_path,
600
+ old_key_path=old_key_path,
601
+ output_path=destination,
602
+ format_version=args.format_version,
603
+ overwrite=args.force,
604
+ )
605
+ except Exception as exc:
606
+ print(f"Failed to rekey {source_path}: {exc}", file=sys.stderr)
607
+ return 1
608
+
609
+ print(str(result_path))
610
+ return 0
611
+
612
+
613
+ def main(argv: list[str] | None = None) -> int:
614
+ args = list(sys.argv[1:] if argv is None else argv)
615
+ if args and args[0] == "inspect":
616
+ parsed = _build_inspect_parser().parse_args(args[1:])
617
+ return _run_inspect(parsed)
618
+ if args and args[0] == "decrypt":
619
+ parsed = _build_decrypt_parser().parse_args(args[1:])
620
+ return _run_decrypt(parsed)
621
+ if args and args[0] == "add":
622
+ parsed = _build_add_parser().parse_args(args[1:])
623
+ return _run_add(parsed)
624
+ if args and args[0] == "delete":
625
+ parsed = _build_delete_parser().parse_args(args[1:])
626
+ return _run_delete(parsed)
627
+ if args and args[0] == "migrate":
628
+ parsed = _build_migrate_parser().parse_args(args[1:])
629
+ return _run_migrate(parsed)
630
+ if args and args[0] == "rekey":
631
+ parsed = _build_rekey_parser().parse_args(args[1:])
632
+ return _run_rekey(parsed)
633
+ if args and args[0] == "encrypt":
634
+ parsed = _build_encrypt_parser().parse_args(args[1:])
635
+ return _run_encrypt(parsed)
636
+ parsed = _build_encrypt_parser().parse_args(args)
637
+ return _run_encrypt(parsed)
638
+
639
+
640
+ if __name__ == "__main__":
641
+ raise SystemExit(main())