Skip to main content

sioul/
pgp.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! OpenPGP keys from the terminal: `sioul pgp list|make|import|export|lookup|cards`.
5
6use crate::Session;
7use crate::accounts::prompt_password;
8use clap::Subcommand;
9use sioul_core::pgp;
10use sioul_core::securitykey::{self, Purpose, Touch};
11use sioul_sync::{keys, secret};
12use std::path::PathBuf;
13
14#[derive(Subcommand)]
15pub(crate) enum PgpCommand {
16    /// Your keys, then others'.
17    List,
18    /// Makes a key for one of your addresses; the keyring keeps its passphrase.
19    /// Says its fingerprint, then where its revocation certificate is kept
20    /// (beside it): copy that file somewhere safe, apart from this computer.
21    Make { address: String },
22    /// Imports the keys of a file; a secret key's passphrase is asked, then kept in the keyring.
23    Import { file: PathBuf },
24    /// Writes a public key, armored, to the terminal.
25    Export { fingerprint: String },
26    /// Looks up someone's key: their domain's Web Key Directory, then keys.openpgp.org.
27    Lookup { address: String },
28    /// What each security key plugged in says, without its PIN: its keys, their
29    /// touch, the tries left. Nothing is signed or opened, no PIN is given.
30    Cards,
31}
32
33pub(crate) fn run(s: &Session, command: PgpCommand) -> Result<(), String> {
34    match command {
35        PgpCommand::List => {
36            for key in pgp::summaries() {
37                let mine = if key.own { "*" } else { " " };
38                println!("{mine} {} · {} · {}{}", key.fingerprint, crate::one_line(&key.names.join(", ")), key.created, if key.expires.is_empty() { String::new() } else { format!(" → {}", key.expires) });
39            }
40            Ok(())
41        }
42        PgpCommand::Make { address } => {
43            let name = s.config.accounts.iter().find(|a| a.address.as_deref() == Some(address.as_str())).and_then(|a| a.name.clone()).unwrap_or_default();
44            let passphrase = match secret::test_passphrase() {
45                Some(fixed) => fixed,
46                None => pgp::new_passphrase()?,
47            };
48            let made = pgp::generate(&name, &address, &passphrase)?;
49            secret::save_pgp_passphrase(&made.fingerprint, &passphrase).map_err(|e| e.sentence(&s.tr, &address))?;
50            println!("{}", made.fingerprint);
51            println!("{}", pgp::revocation_path(&made.fingerprint).display());
52            Ok(())
53        }
54        PgpCommand::Import { file } => {
55            let bytes = std::fs::read(&file).map_err(|e| format!("{}: {e}", file.display()))?;
56            // Your keys before: one imported again with a mistyped passphrase stays yours.
57            let had: Vec<String> = pgp::summaries().into_iter().filter(|k| k.own).map(|k| k.fingerprint).collect();
58            for key in pgp::import(&bytes)? {
59                if key.own {
60                    let passphrase = prompt_password(s, &crate::one_line(&key.names.join(", ")))?;
61                    if !pgp::opens(&key.fingerprint, &passphrase) {
62                        if !had.contains(&key.fingerprint) {
63                            let _ = pgp::remove(&key.fingerprint);
64                        }
65                        return Err(s.tr.text("pgp-wrong-passphrase", None));
66                    }
67                    secret::save_pgp_passphrase(&key.fingerprint, &passphrase).map_err(|e| e.sentence(&s.tr, &key.fingerprint))?;
68                }
69                println!("{} · {}", key.fingerprint, crate::one_line(&key.names.join(", ")));
70            }
71            Ok(())
72        }
73        PgpCommand::Export { fingerprint } => {
74            print!("{}", pgp::export_public(&fingerprint).ok_or_else(|| s.tr.text("pgp-no-key", None))?);
75            Ok(())
76        }
77        PgpCommand::Cards => {
78            let cards = securitykey::cards(sioul_sync::securitykey::readers()).map_err(|e| e.sentence(&s.tr, Purpose::Read))?;
79            let known = securitykey::known_cards();
80            let touch = |touch: Touch| {
81                let policy = match touch {
82                    Touch::Off => "off",
83                    Touch::On => "on",
84                    Touch::Fixed => "fixed",
85                    Touch::Cached => "cached",
86                    Touch::CachedFixed => "cached-fixed",
87                    Touch::Unknown => "unknown",
88                };
89                s.say("seckey-cli-touch", &[("policy", policy.to_string())])
90            };
91            for card in cards {
92                // Known to Sioul, as `list` marks your keys.
93                let mine = if known.iter().any(|k| k.ident.eq_ignore_ascii_case(&card.ident)) { "*" } else { " " };
94                println!("{mine} {} · {} · {}", card.ident, card.label(&s.tr), crate::one_line(&card.holder));
95                for (id, slot) in [("seckey-cli-signs", &card.sign), ("seckey-cli-decrypts", &card.decrypt)] {
96                    if let Some(slot) = slot {
97                        let date = securitykey::full_date(&s.tr, i64::from(slot.created));
98                        println!("    {}", s.say(id, &[("fingerprint", slot.fingerprint.clone()), ("algorithm", slot.algorithm.clone()), ("date", date), ("touch", touch(slot.touch))]));
99                    }
100                }
101                println!("    {}", s.say("seckey-tries-left", &[("n", card.pin_tries.to_string())]));
102                if card.pin_each_signature {
103                    println!("    {}", s.tr.text("seckey-pin-each", None));
104                }
105                println!("    {}", s.say("seckey-cli-codes", &[("reset", card.reset_tries.to_string()), ("admin", card.admin_tries.to_string())]));
106                if !card.url.is_empty() {
107                    println!("    {}", crate::one_line(&card.url));
108                }
109                println!("    {}", crate::one_line(&card.reader));
110            }
111            Ok(())
112        }
113        PgpCommand::Lookup { address } => {
114            let found = keys::lookup(&address).map_err(|e| e.sentence(&s.tr, &address))?;
115            if found.is_empty() {
116                println!("{}", s.say("pgp-lookup-some", &[("missing", address)]));
117            }
118            for key in found {
119                println!("{} · {}", key.fingerprint, crate::one_line(&key.names.join(", ")));
120            }
121            Ok(())
122        }
123    }
124}