pasejo 2026.2.22

passage re-implementation in Rust for teams
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
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
// SPDX-FileCopyrightText: The pasejo Authors
// SPDX-License-Identifier: 0BSD

use std::path::PathBuf;

use crate::cli::completer;
use crate::cli::parser;
use crate::models::password_store::{OneTimePasswordAlgorithm, OneTimePasswordType};
use clap::ValueHint::{DirPath, FilePath};
use clap::{Args, Parser, Subcommand};
use clap_verbosity_flag::InfoLevel;
use serde::{Deserialize, Serialize};

/// age-backed password manager for teams
#[derive(Parser)]
#[command(version)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,

    #[command(flatten)]
    pub verbose: clap_verbosity_flag::Verbosity<InfoLevel>,

    /// Work offline, do not synchronize with remote stores
    #[arg(short = 'O', long)]
    pub offline: bool,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Manage pasejo configuration
    Config {
        #[command(subcommand)]
        command: ConfigCommands,
    },

    /// Export passwords
    Export {
        #[command(subcommand)]
        command: ExportCommands,
    },

    /// Manage hooks
    Hook {
        #[command(subcommand)]
        command: HookCommands,
    },

    /// Manage identities
    Identity {
        #[command(subcommand)]
        command: IdentityCommands,
    },

    /// Manage one-time passwords
    Otp {
        #[command(subcommand)]
        command: OtpCommands,
    },

    /// Manage recipients
    Recipient {
        #[command(subcommand)]
        command: RecipientCommands,
    },

    /// Manage secrets
    Secret {
        #[command(subcommand)]
        command: SecretCommands,
    },

    /// Manage stores
    Store {
        #[command(subcommand)]
        command: StoreCommands,
    },
}

#[derive(Subcommand)]
pub enum ConfigCommands {
    /// Get a configuration value
    Get(ConfigGetArgs),

    /// Set a configuration value
    Set(ConfigSetArgs),
}

#[derive(Args)]
pub struct ConfigGetArgs {
    /// Name of the configuration option to get
    pub option: ConfigurationOption,
}

#[derive(Args)]
pub struct ConfigSetArgs {
    /// Name of the configuration option to set
    pub option: ConfigurationOption,

    /// Value to set the configuration option to
    pub value: String,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, clap::ValueEnum)]
pub enum ConfigurationOption {
    IgnoreMissingIdentities,
    ClipboardTimeout,
    PullIntervalSeconds,
    PushIntervalSeconds,
}

#[derive(Subcommand)]
pub enum ExportCommands {
    /// Export all passwords of a store in Bitwarden JSON format
    Bitwarden(BitwardenArgs),
}

#[derive(Args)]
pub struct BitwardenArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The organization ID to use. When set, outputs organization JSON format
    #[arg(long)]
    pub organization_id: Option<String>,

    /// The collection ID to use
    #[arg(long, requires = "organization_id")]
    pub collection_id: Option<String>,

    /// The collection name to use
    #[arg(long, requires = "organization_id")]
    pub collection_name: Option<String>,

    #[arg(long, default_values_t = [String::from("login"), String::from("email"), String::from("username")])]
    pub username_keys: Vec<String>,

    #[arg(long, default_values_t = [String::from("uri"), String::from("url"), String::from("link"), String::from("site")])]
    pub uri_keys: Vec<String>,

    /// Toggle whether to print pretty JSON or not
    #[arg(long, default_missing_value="true", default_value("false"), num_args=0..=1)]
    pub pretty: Option<bool>,
}

#[derive(Subcommand)]
pub enum HookCommands {
    /// Set hook commands to a store or globally to all stores
    Set(HookSetArgs),

    /// Get configured hook commands
    Get(HookGetArgs),

    /// Run configured hook commands
    Run(HookRunArgs),
}

#[derive(Args)]
pub struct HookSetArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Add commands to the global configuration or save them per store.
    #[arg(short, long, conflicts_with = "store")]
    pub global: bool,

    /// The pull command(s) to set
    #[arg(long)]
    pub pull: Vec<String>,

    /// The push command(s) to set
    #[arg(long)]
    pub push: Vec<String>,

    /// Prepend commands instead of replacing them
    #[arg(long, conflicts_with = "append")]
    pub prepend: bool,

    /// Append commands instead of replacing them
    #[arg(long, conflicts_with = "prepend")]
    pub append: bool,
}

#[derive(Args)]
pub struct HookGetArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Add commands to the global configuration or save them per store.
    #[arg(short, long, conflicts_with = "store")]
    pub global: bool,
}

#[derive(Args)]
pub struct HookRunArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Toggle whether hooks should be executed in all stores
    #[arg(
        long,
        default_missing_value = "true",
        default_value("false"),
        num_args=0..=1,
        conflicts_with = "store"
    )]
    pub all: Option<bool>,

    /// Toggle whether changes from the remote store should be pulled
    #[arg(long, default_missing_value="true", default_value("false"), num_args=0..=1)]
    pub pull: Option<bool>,

    /// Toggle whether local changes should be pushed to the remote store
    #[arg(long, default_missing_value="true", default_value("false"), num_args=0..=1)]
    pub push: Option<bool>,
}

#[derive(Subcommand)]
pub enum OtpCommands {
    /// Adds a one-time password
    Add(OtpAddArgs),

    /// Remove a one-time password
    Remove(OtpRemoveArgs),

    /// List one-time passwords
    List(OtpListArgs),

    /// Show a one-time password
    Show(OtpShowArgs),

    /// Copy a one-time password from old-path to new-path
    Copy(OtpCopyArgs),

    /// Move a one-time password from old-path to new-path
    Move(OtpMoveArgs),
}

#[derive(Args)]
pub struct OtpAddArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing one-time password without prompting
    #[arg(short, long)]
    pub force: bool,

    /// Parse an otpauth URL
    #[arg(long, conflicts_with_all = ["qrcode"])]
    pub url: Option<String>,

    /// Parse a QR code containing an otpauth URL
    #[arg(long, value_hint = FilePath, value_parser = parser::existing_file, conflicts_with_all = ["url"])]
    pub qrcode: Option<PathBuf>,

    /// The base secret of the one-time password
    #[arg(long, conflicts_with_all = ["url", "qrcode"])]
    pub secret: Option<String>,

    /// The type of the one-time password
    #[arg(long = "type", conflicts_with_all = ["url", "qrcode"])]
    pub otp_type: Option<OneTimePasswordType>,

    /// The algorithm of the one-time password
    #[arg(long, conflicts_with_all = ["url", "qrcode"])]
    pub algorithm: Option<OneTimePasswordAlgorithm>,

    /// The digits of the one-time password
    #[arg(long, conflicts_with_all = ["url", "qrcode"])]
    pub digits: Option<u8>,

    /// The period of the one-time password
    #[arg(long, conflicts_with_all = ["url", "qrcode", "counter"])]
    pub period: Option<u64>,

    /// The skew of the one-time password
    #[arg(long, group = "manual", conflicts_with_all = ["url", "qrcode", "counter"])]
    pub skew: Option<u64>,

    /// The counter of the one-time password
    #[arg(long, group = "manual", conflicts_with_all = ["url", "qrcode", "period"])]
    pub counter: Option<u64>,

    /// The path of the one-time password within the selected store
    pub password_path: String,
}

#[derive(Args)]
pub struct OtpRemoveArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Delete an existing one-time password without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The path of the one-time password within the selected store
    pub password_path: String,
}

#[derive(Args)]
pub struct OtpListArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Toggle to display one-time passwords as a tree
    #[arg(short, long)]
    pub tree: bool,
}

#[derive(Args)]
pub struct OtpShowArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Copy one-time password to clipboard
    #[arg(short, long)]
    pub clip: bool,

    /// The path of the one-time password within the selected store
    pub password_path: String,
}

#[derive(Args)]
pub struct OtpCopyArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing one-time password without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The source path of the one-time password
    pub source_path: String,

    /// The target path of the one-time password
    pub target_path: String,
}

#[derive(Args)]
pub struct OtpMoveArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing one-time password without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The current path of the secret
    pub current_path: String,

    /// The new path of the secret
    pub new_path: String,
}

#[derive(Subcommand)]
pub enum IdentityCommands {
    /// Adds an identity either to a single store or to your global
    /// configuration file.
    Add(IdentityAddArgs),

    /// Remove an identity
    Remove(IdentityRemoveArgs),

    /// List identities
    List(IdentityListArgs),
}

#[derive(Args)]
pub struct IdentityAddArgs {
    /// The path to the identity file
    #[arg(short, long, value_hint = FilePath, value_parser = parser::existing_file)]
    pub file: PathBuf,

    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Add to the global configuration file when enabled, otherwise add to
    /// store
    #[arg(short, long, conflicts_with = "store")]
    pub global: bool,
}

#[derive(Args)]
pub struct IdentityRemoveArgs {
    /// The path to the identity file
    #[arg(short, long, value_hint = FilePath)]
    pub file: PathBuf,

    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Remove from the global configuration file when enabled, otherwise remove
    /// from store
    #[arg(short, long, conflicts_with = "store")]
    pub global: bool,

    /// Don't fail on unknown identities
    #[arg(short, long)]
    pub ignore_unknown: bool,
}

#[derive(Args)]
pub struct IdentityListArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Show only the global identities
    #[arg(short, long)]
    pub global: bool,
}

#[derive(Subcommand)]
pub enum RecipientCommands {
    /// Adds a recipient
    Add(RecipientAddArgs),

    /// Remove a recipient
    Remove(RecipientRemoveArgs),

    /// Lists all recipients
    List(RecipientListArgs),
}

#[derive(Args)]
pub struct RecipientAddArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    #[command(flatten)]
    pub keys: RecipientKeysArgs,

    /// The name of the new recipient
    #[arg(short, long)]
    pub name: Option<String>,
}

#[derive(Args)]
#[group(required = true, multiple = false)]
pub struct RecipientKeysArgs {
    /// The public key of the new recipient
    #[arg(short, long)]
    pub public_key: Option<String>,

    /// Read public key of recipient from a file
    #[arg(short, long)]
    pub file: Option<String>,

    /// The Codeberg username to add as recipient
    #[arg(long)]
    pub codeberg: Option<String>,

    /// The GitHub username to add as recipient
    #[arg(long)]
    pub github: Option<String>,

    /// The GitLab username to add as recipient
    #[arg(long)]
    pub gitlab: Option<String>,
}

#[derive(Args)]
pub struct RecipientRemoveArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Don't fail on unknown recipients
    #[arg(short, long)]
    pub ignore_unknown: bool,

    /// The public key of the recipient to remove
    pub public_key: String,
}

#[derive(Args)]
pub struct RecipientListArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,
}

#[derive(Subcommand)]
pub enum SecretCommands {
    /// Add a new secret or overwrite an existing one
    Add(SecretAddArgs),

    /// Audit password strength of secrets
    Audit(SecretAuditArgs),

    /// Copy secret from old-path to new-path
    Copy(SecretCopyArgs),

    /// Edit an existing secret
    Edit(SecretEditArgs),

    /// Generate a secret and add it into the store
    Generate(SecretGenerateArgs),

    /// Grep for a search-string in secrets when decrypted
    Grep(SecretGrepArgs),

    /// List all secrets
    List(SecretListArgs),

    /// Move secret from old-path to new-path
    Move(SecretMoveArgs),

    /// Remove an existing secret
    Remove(SecretRemoveArgs),

    /// Show secret
    Show(SecretShowArgs),
}

#[derive(Args)]
pub struct SecretAddArgs {
    /// Toggle multiline edit mode
    #[arg(short, long)]
    pub multiline: bool,

    /// Overwrite an existing secrets without prompting
    #[arg(short, long)]
    pub force: bool,

    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The path of the secret within the selected store
    pub secret_path: String,
}

#[derive(Args)]
pub struct SecretAuditArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The path of the secret within the selected store
    pub secret_path: Option<String>,
}

#[derive(Args)]
pub struct SecretCopyArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing secrets without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The path of an existing secret
    pub source_path: String,

    /// The target path for the copied secret
    pub target_path: String,
}

#[derive(Args)]
pub struct SecretEditArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The path of the secret within the selected store
    pub secret_path: String,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct SecretGenerateArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing secrets without prompting
    #[arg(short, long)]
    pub force: bool,

    /// Overwrite just the password of an existing secret without prompting
    #[arg(short, long)]
    pub inplace: bool,

    /// The path of the secret within the selected store
    pub secret_path: String,

    /// The length of the generated passwords.
    #[arg(short, long, default_value_t = 25)]
    pub length: usize,

    /// Passwords are allowed to, or must if the strict is true, contain a number.
    #[arg(short, long, default_value_t = true)]
    pub numbers: bool,

    /// Passwords are allowed to, or must if the strict is true, contain a lowercase letter.
    #[arg(short = 'j', long, default_value_t = true)]
    pub lowercase_letters: bool,

    /// Passwords are allowed to, or must if the strict is true, contain an uppercase letter.
    #[arg(short, long, default_value_t = true)]
    pub uppercase_letters: bool,

    /// Passwords are allowed to, or must if the strict is true, contain a symbol.
    #[arg(short = 'y', long, default_value_t = false)]
    pub symbols: bool,

    /// Passwords are allowed to, or must if the strict is true, contain a space.
    #[arg(short = 'w', long, default_value_t = false)]
    pub spaces: bool,

    /// Whether to exclude similar characters, iI1loO0"'`|`
    #[arg(short, long, default_value_t = true)]
    pub exclude_similar_characters: bool,

    /// Whether the password rules are strict.
    #[arg(short = 't', long, default_value_t = true)]
    pub strict: bool,
}

#[derive(Args)]
pub struct SecretGrepArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Whether the search string should be used as a regular expression
    #[arg(short, long)]
    pub regex: bool,

    /// The string to search in all secrets
    pub search_string: String,
}

#[derive(Args)]
pub struct SecretListArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Toggle to display secrets as a tree
    #[arg(short, long)]
    pub tree: bool,
}

#[derive(Args)]
pub struct SecretMoveArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite an existing secrets without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The current path of the secret
    pub current_path: String,

    /// The new path of the secret
    pub new_path: String,
}

#[derive(Args)]
pub struct SecretRemoveArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Delete an existing secrets without prompting
    #[arg(short, long)]
    pub force: bool,

    /// The path of the secret within the selected store
    pub secret_path: String,
}

#[derive(Args)]
pub struct SecretShowArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Toggle to display secrets as QR code
    #[arg(short = 'o', long, conflicts_with = "clip")]
    pub qrcode: bool,

    /// Copy secret to clipboard
    #[arg(short, long, conflicts_with = "qrcode")]
    pub clip: bool,

    /// Show only the specified line, or skip lines when given a negative number
    #[arg(short, long)]
    pub line: Option<isize>,

    /// The path of the secret within the selected store
    pub secret_path: String,
}

#[derive(Subcommand)]
pub enum StoreCommands {
    /// Adds a new store
    Add(StoreAddArgs),

    /// Decrypt a store and print its content
    Decrypt(StoreDecryptArgs),

    /// Executes a command inside the directory of a store
    Exec(StoreExecArgs),

    /// List all available stores
    List(StoreListArgs),

    /// Merge two stores
    Merge(StoreMergeArgs),

    /// Remove an existing store
    Remove(StoreRemoveArgs),

    /// Mark a store as default
    SetDefault(StoreSetDefaultArgs),
}

#[derive(Args)]
pub struct StoreAddArgs {
    /// The path on your local system for the new store
    #[arg(short, long, value_hint = DirPath)]
    pub path: PathBuf,

    /// The name for the new store
    #[arg(short, long)]
    pub name: String,

    /// Whether the new store should be the default store
    #[arg(short, long)]
    pub default: bool,
}

#[derive(Args)]
pub struct StoreRemoveArgs {
    /// Optional name of store to use. Defaults to the default store or the
    /// first one defined in the local user configuration
    #[arg(add = completer::store_name(), value_parser = parser::store_name)]
    pub store: Option<String>,

    /// Whether the store should be removed from the local file system
    #[arg(short, long)]
    pub remove_data: bool,
}

#[derive(Args)]
pub struct StoreSetDefaultArgs {
    /// The name of the store to use as default
    #[arg(value_parser = parser::store_name)]
    pub name: String,
}

#[derive(Args)]
pub struct StoreExecArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The command to execute inside the store
    #[arg(num_args=0..)]
    pub command: Vec<String>,
}

#[derive(Args)]
pub struct StoreDecryptArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// Overwrite the path to the store
    #[arg(long, value_hint = FilePath)]
    pub store_path: Option<PathBuf>,
}

#[derive(Args)]
pub struct StoreMergeArgs {
    #[command(flatten)]
    pub store_selection: StoreSelectionArgs,

    /// The path to the common ancestor of the two stores
    #[arg(long, value_hint = FilePath)]
    pub common_ancestor: PathBuf,

    /// The path to current version of the store
    #[arg(long, value_hint = FilePath)]
    pub current_version: PathBuf,

    /// The path to the other version of the store
    #[arg(long, value_hint = FilePath)]
    pub other_version: PathBuf,
}

#[derive(Args)]
pub struct StoreListArgs {}

#[derive(Args)]
pub struct StoreSelectionArgs {
    /// Optional name of store to use. Defaults to the default store or the
    /// first one defined in the local user configuration
    #[arg(short, long, add = completer::store_name(), value_parser = parser::store_name)]
    pub store: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Cli::command().debug_assert();
    }
}