#
pacman_key/
keyring.rs

    1use std::process::Stdio;
    2use tokio::io::{AsyncBufReadExt, BufReader};
    3use tokio::process::Command;
    4
    5use crate::error::{Error, Result};
    6use crate::parse::{parse_keys, parse_signatures};
    7use crate::types::{Key, RefreshOptions, RefreshProgress, Signature};
    8use crate::validation::{validate_keyid, validate_keyring_name};
    9
    10const DEFAULT_GPG_HOMEDIR: &str = "/etc/pacman.d/gnupg";
    11
    12/// Read-only interface for querying a GPG keyring.
    13///
    14/// This type is returned by [`Keyring::with_homedir`] and only provides
    15/// read operations (`list_keys`, `list_signatures`). Write operations
    16/// require a [`Keyring`] which targets the system pacman keyring.
    17///
    18/// # Example
    19///
    20/// ```no_run
    21/// # async fn example() -> pacman_key::Result<()> {
    22/// use pacman_key::Keyring;
    23///
    24/// let reader = Keyring::with_homedir("/custom/gnupg");
    25/// let keys = reader.list_keys().await?;
    26/// # Ok(())
    27/// # }
    28/// ```
    29pub struct ReadOnlyKeyring {
    30    gpg_homedir: String,
    31}
    32
    33impl ReadOnlyKeyring {
    34    /// Lists all keys in the keyring.
    35    pub async fn list_keys(&self) -> Result<Vec<Key>> {
    36        let output = Command::new("gpg")
    37            .env("LC_ALL", "C")
    38            .arg(format!("--homedir={}", self.gpg_homedir))
    39            .args(["--list-keys", "--with-colons"])
    40            .output()
    41            .await?;
    42
    43        if !output.status.success() {
    44            return Err(check_gpg_error(&self.gpg_homedir, output.status, &output.stderr));
    45        }
    46
    47        let stdout = String::from_utf8_lossy(&output.stdout);
    48        parse_keys(&stdout)
    49    }
    50
    51    /// Lists signatures on keys in the keyring.
    52    ///
    53    /// If `keyid` is provided, lists signatures only for that key.
    54    /// Otherwise lists all signatures in the keyring.
    55    pub async fn list_signatures(&self, keyid: Option<&str>) -> Result<Vec<Signature>> {
    56        let mut cmd = Command::new("gpg");
    57        cmd.env("LC_ALL", "C")
    58            .arg(format!("--homedir={}", self.gpg_homedir))
    59            .args(["--list-sigs", "--with-colons"]);
    60
    61        if let Some(id) = keyid {
    62            let validated = validate_keyid(id)?;
    63            cmd.arg(validated);
    64        }
    65
    66        let output = cmd.output().await?;
    67
    68        if !output.status.success() {
    69            return Err(check_gpg_error(&self.gpg_homedir, output.status, &output.stderr));
    70        }
    71
    72        let stdout = String::from_utf8_lossy(&output.stdout);
    73        parse_signatures(&stdout)
    74    }
    75}
    76
    77fn check_gpg_error(homedir: &str, status: std::process::ExitStatus, stderr: &[u8]) -> Error {
    78    let msg = String::from_utf8_lossy(stderr);
    79
    80    if msg.contains("Permission denied") || msg.contains("permission denied") {
    81        return Error::PermissionDenied;
    82    }
    83
    84    if msg.contains("No such file or directory") && msg.contains(homedir) {
    85        return Error::KeyringNotInitialized;
    86    }
    87
    88    Error::PacmanKey {
    89        status: status.code().unwrap_or(-1),
    90        stderr: msg.to_string(),
    91    }
    92}
    93
    94/// Interface for managing the pacman keyring.
    95///
    96/// Provides async methods for key listing, importing, signing, and keyring management.
    97/// Most write operations require root privileges.
    98///
    99/// # Example
    100///
    101/// ```no_run
    102/// use pacman_key::Keyring;
    103///
    104/// # async fn example() -> pacman_key::Result<()> {
    105/// let keyring = Keyring::new();
    106/// let keys = keyring.list_keys().await?;
    107/// println!("Found {} keys", keys.len());
    108/// # Ok(())
    109/// # }
    110/// ```
    111pub struct Keyring {
    112    gpg_homedir: String,
    113}
    114
    115impl Default for Keyring {
    116    fn default() -> Self {
    117        Self::new()
    118    }
    119}
    120
    121impl Keyring {
    122    /// Creates a new Keyring using the default pacman keyring path.
    123    #[must_use]
    124    pub fn new() -> Self {
    125        Self {
    126            gpg_homedir: DEFAULT_GPG_HOMEDIR.to_string(),
    127        }
    128    }
    129
    130    /// Creates a read-only keyring interface for a custom GPG home directory.
    131    ///
    132    /// Returns a [`ReadOnlyKeyring`] that can only perform read operations
    133    /// (`list_keys`, `list_signatures`). This is useful for inspecting
    134    /// alternative keyrings without risking modifications.
    135    ///
    136    /// # Example
    137    ///
    138    /// ```no_run
    139    /// # async fn example() -> pacman_key::Result<()> {
    140    /// use pacman_key::Keyring;
    141    ///
    142    /// let reader = Keyring::with_homedir("/custom/gnupg");
    143    /// let keys = reader.list_keys().await?;
    144    /// # Ok(())
    145    /// # }
    146    /// ```
    147    #[must_use]
    148    pub fn with_homedir(path: impl Into<String>) -> ReadOnlyKeyring {
    149        ReadOnlyKeyring {
    150            gpg_homedir: path.into(),
    151        }
    152    }
    153
    154    async fn run_pacman_key<I, S>(&self, args: I) -> Result<()>
    155    where
    156        I: IntoIterator<Item = S>,
    157        S: AsRef<std::ffi::OsStr>,
    158    {
    159        let output = Command::new("pacman-key")
    160            .env("LC_ALL", "C")
    161            .args(args)
    162            .output()
    163            .await?;
    164
    165        if !output.status.success() {
    166            return Err(self.check_error(output.status, &output.stderr));
    167        }
    168
    169        Ok(())
    170    }
    171
    172    /// Lists all keys in the keyring.
    173    ///
    174    /// # Example
    175    ///
    176    /// ```no_run
    177    /// # async fn example() -> pacman_key::Result<()> {
    178    /// use pacman_key::Keyring;
    179    ///
    180    /// let keyring = Keyring::new();
    181    /// for key in keyring.list_keys().await? {
    182    ///     println!("{} - {:?}", key.uid, key.validity);
    183    /// }
    184    /// # Ok(())
    185    /// # }
    186    /// ```
    187    pub async fn list_keys(&self) -> Result<Vec<Key>> {
    188        let output = Command::new("gpg")
    189            .env("LC_ALL", "C")
    190            .arg(format!("--homedir={}", self.gpg_homedir))
    191            .args(["--list-keys", "--with-colons"])
    192            .output()
    193            .await?;
    194
    195        if !output.status.success() {
    196            return Err(self.check_error(output.status, &output.stderr));
    197        }
    198
    199        let stdout = String::from_utf8_lossy(&output.stdout);
    200        parse_keys(&stdout)
    201    }
    202
    203    /// Lists signatures on keys in the keyring.
    204    ///
    205    /// If `keyid` is provided, lists signatures only for that key.
    206    /// Otherwise lists all signatures in the keyring.
    207    pub async fn list_signatures(&self, keyid: Option<&str>) -> Result<Vec<Signature>> {
    208        let mut cmd = Command::new("gpg");
    209        cmd.env("LC_ALL", "C")
    210            .arg(format!("--homedir={}", self.gpg_homedir))
    211            .args(["--list-sigs", "--with-colons"]);
    212
    213        if let Some(id) = keyid {
    214            let validated = validate_keyid(id)?;
    215            cmd.arg(validated);
    216        }
    217
    218        let output = cmd.output().await?;
    219
    220        if !output.status.success() {
    221            return Err(self.check_error(output.status, &output.stderr));
    222        }
    223
    224        let stdout = String::from_utf8_lossy(&output.stdout);
    225        parse_signatures(&stdout)
    226    }
    227
    228    /// Initializes the pacman keyring.
    229    ///
    230    /// Creates the keyring directory and generates a local signing key.
    231    /// Requires root privileges.
    232    pub async fn init_keyring(&self) -> Result<()> {
    233        self.run_pacman_key(&["--init"]).await
    234    }
    235
    236    /// Populates the keyring with keys from distribution keyrings.
    237    ///
    238    /// If no keyrings are specified, defaults to "archlinux".
    239    /// Requires root privileges.
    240    ///
    241    /// # Keyring Names
    242    ///
    243    /// Keyring names must contain only alphanumeric characters, hyphens, or
    244    /// underscores. Common valid names include "archlinux", "archlinuxarm",
    245    /// and "manjaro".
    246    pub async fn populate(&self, keyrings: &[&str]) -> Result<()> {
    247        for name in keyrings {
    248            validate_keyring_name(name)?;
    249        }
    250
    251        let keyring_args: &[&str] = if keyrings.is_empty() {
    252            &["archlinux"]
    253        } else {
    254            keyrings
    255        };
    256
    257        self.run_pacman_key(
    258            std::iter::once("--populate").chain(keyring_args.iter().copied()),
    259        )
    260        .await
    261    }
    262
    263    /// Receives keys from a keyserver.
    264    ///
    265    /// Requires root privileges.
    266    ///
    267    /// # Note
    268    ///
    269    /// This function validates key IDs before making the request. Key IDs must be
    270    /// 8, 16, or 40 hexadecimal characters (with optional "0x" prefix).
    271    pub async fn receive_keys(&self, keyids: &[&str]) -> Result<()> {
    272        if keyids.is_empty() {
    273            return Ok(());
    274        }
    275
    276        let validated: Vec<String> = keyids
    277            .iter()
    278            .map(|k| validate_keyid(k))
    279            .collect::<Result<_>>()?;
    280
    281        self.run_pacman_key(
    282            std::iter::once("--recv-keys".to_string()).chain(validated),
    283        )
    284        .await
    285    }
    286
    287    /// Locally signs a key to mark it as trusted.
    288    ///
    289    /// Requires root privileges.
    290    ///
    291    /// # Note
    292    ///
    293    /// This function validates key IDs before making the request. Key IDs must be
    294    /// 8, 16, or 40 hexadecimal characters (with optional "0x" prefix).
    295    pub async fn locally_sign_key(&self, keyid: &str) -> Result<()> {
    296        let validated = validate_keyid(keyid)?;
    297        self.run_pacman_key(&["--lsign-key", &validated]).await
    298    }
    299
    300    /// Deletes a key from the keyring.
    301    ///
    302    /// Requires root privileges.
    303    ///
    304    /// # Note
    305    ///
    306    /// This function validates key IDs before making the request. Key IDs must be
    307    /// 8, 16, or 40 hexadecimal characters (with optional "0x" prefix).
    308    pub async fn delete_key(&self, keyid: &str) -> Result<()> {
    309        let validated = validate_keyid(keyid)?;
    310        self.run_pacman_key(&["--delete", &validated]).await
    311    }
    312
    313    /// Refreshes all keys from the keyserver.
    314    ///
    315    /// This is a long-running operation. The callback receives progress updates
    316    /// as keys are refreshed.
    317    ///
    318    /// # Example
    319    ///
    320    /// ```no_run
    321    /// # async fn example() -> pacman_key::Result<()> {
    322    /// use pacman_key::{Keyring, RefreshOptions, RefreshProgress};
    323    ///
    324    /// let keyring = Keyring::new();
    325    ///
    326    /// // With timeout
    327    /// let options = RefreshOptions { timeout_secs: Some(300) };
    328    ///
    329    /// keyring.refresh_keys(|progress| {
    330    ///     match progress {
    331    ///         RefreshProgress::Starting { total_keys } => {
    332    ///             println!("Refreshing {} keys...", total_keys);
    333    ///         }
    334    ///         RefreshProgress::Refreshing { current, total, keyid } => {
    335    ///             println!("[{}/{}] {}", current, total, keyid);
    336    ///         }
    337    ///         RefreshProgress::Completed => println!("Done!"),
    338    ///         RefreshProgress::Error { keyid, message } => {
    339    ///             eprintln!("Error refreshing {}: {}", keyid, message);
    340    ///         }
    341    ///         _ => {}
    342    ///     }
    343    /// }, options).await?;
    344    /// # Ok(())
    345    /// # }
    346    /// ```
    347    pub async fn refresh_keys<F>(
    348        &self,
    349        callback: F,
    350        options: RefreshOptions,
    351    ) -> Result<()>
    352    where
    353        F: Fn(RefreshProgress),
    354    {
    355        let refresh_future = self.refresh_keys_inner(&callback);
    356
    357        match options.timeout_secs {
    358            Some(secs) => {
    359                tokio::time::timeout(std::time::Duration::from_secs(secs), refresh_future)
    360                    .await
    361                    .map_err(|_| Error::Timeout(secs))?
    362            }
    363            None => refresh_future.await,
    364        }
    365    }
    366
    367    async fn refresh_keys_inner<F>(&self, callback: &F) -> Result<()>
    368    where
    369        F: Fn(RefreshProgress),
    370    {
    371        let keys = self.list_keys().await?;
    372        let total = keys.len();
    373
    374        callback(RefreshProgress::Starting { total_keys: total });
    375
    376        let mut child = Command::new("pacman-key")
    377            .env("LC_ALL", "C")
    378            .arg("--refresh-keys")
    379            .stdout(Stdio::null())
    380            .stderr(Stdio::piped())
    381            .spawn()?;
    382
    383        let stderr = child.stderr.take().ok_or(Error::StderrCaptureFailed)?;
    384        let mut reader = BufReader::new(stderr).lines();
    385
    386        let mut current = 0;
    387        while let Some(line) = reader.next_line().await? {
    388            if line.contains("refreshing") || line.contains("requesting") {
    389                current += 1;
    390                let keyid = extract_keyid_from_line(&line);
    391                callback(RefreshProgress::Refreshing {
    392                    current,
    393                    total,
    394                    keyid,
    395                });
    396            } else if line.contains("error")
    397                || line.contains("failed")
    398                || line.contains("not found")
    399            {
    400                let keyid = extract_keyid_from_line(&line);
    401                callback(RefreshProgress::Error {
    402                    keyid,
    403                    message: line.clone(),
    404                });
    405            }
    406        }
    407
    408        let status = child.wait().await?;
    409        if !status.success() {
    410            return Err(Error::PacmanKey {
    411                status: status.code().unwrap_or(-1),
    412                stderr: "refresh failed".to_string(),
    413            });
    414        }
    415
    416        callback(RefreshProgress::Completed);
    417        Ok(())
    418    }
    419
    420    fn check_error(&self, status: std::process::ExitStatus, stderr: &[u8]) -> Error {
    421        check_gpg_error(&self.gpg_homedir, status, stderr)
    422    }
    423}
    424
    425fn extract_keyid_from_line(line: &str) -> String {
    426    for word in line.split_whitespace().rev() {
    427        let trimmed = word.trim_end_matches([':', ',', '.']);
    428        let normalized = trimmed
    429            .strip_prefix("0x")
    430            .or_else(|| trimmed.strip_prefix("0X"))
    431            .unwrap_or(trimmed);
    432        if !normalized.is_empty()
    433            && normalized.chars().all(|c| c.is_ascii_hexdigit())
    434            && matches!(normalized.len(), 8 | 16 | 40)
    435        {
    436            return trimmed.to_uppercase();
    437        }
    438    }
    439    String::new()
    440}
    441
    442#[cfg(test)]
    443mod tests {
    444    use super::*;
    445
    446    #[test]
    447    fn test_extract_keyid_from_refresh_line() {
    448        assert_eq!(
    449            extract_keyid_from_line("gpg: refreshing 1 key from hkps://keyserver.ubuntu.com"),
    450            ""
    451        );
    452        assert_eq!(
    453            extract_keyid_from_line(
    454                "gpg: requesting key DEADBEEF from hkps://keyserver.ubuntu.com"
    455            ),
    456            "DEADBEEF"
    457        );
    458        assert_eq!(
    459            extract_keyid_from_line("gpg: key 786C63F330D7CB92: public key imported"),
    460            "786C63F330D7CB92"
    461        );
    462    }
    463
    464    #[test]
    465    fn test_extract_keyid_lowercase_normalized() {
    466        assert_eq!(
    467            extract_keyid_from_line("gpg: key deadbeef: something"),
    468            "DEADBEEF"
    469        );
    470    }
    471
    472    #[test]
    473    fn test_extract_keyid_no_match() {
    474        assert_eq!(extract_keyid_from_line("gpg: some other message"), "");
    475        assert_eq!(extract_keyid_from_line(""), "");
    476    }
    477
    478    #[test]
    479    fn test_check_error_permission_denied() {
    480        let keyring = Keyring::new();
    481        let stderr = b"gpg: Permission denied";
    482        let status = std::process::Command::new("false").status().unwrap();
    483
    484        let err = keyring.check_error(status, stderr);
    485        assert!(matches!(err, Error::PermissionDenied));
    486    }
    487
    488    #[test]
    489    fn test_check_error_permission_denied_lowercase() {
    490        let keyring = Keyring::new();
    491        let stderr = b"gpg: permission denied (are you root?)";
    492        let status = std::process::Command::new("false").status().unwrap();
    493
    494        let err = keyring.check_error(status, stderr);
    495        assert!(matches!(err, Error::PermissionDenied));
    496    }
    497
    498    #[test]
    499    fn test_check_error_keyring_not_initialized() {
    500        let keyring = Keyring::new();
    501        let stderr = b"gpg: keybox '/etc/pacman.d/gnupg/pubring.kbx': No such file or directory";
    502        let status = std::process::Command::new("false").status().unwrap();
    503
    504        let err = keyring.check_error(status, stderr);
    505        assert!(matches!(err, Error::KeyringNotInitialized));
    506    }
    507
    508    #[test]
    509    fn test_check_error_generic() {
    510        let keyring = Keyring::new();
    511        let stderr = b"gpg: some unknown error";
    512        let status = std::process::Command::new("false").status().unwrap();
    513
    514        let err = keyring.check_error(status, stderr);
    515        match err {
    516            Error::PacmanKey { status: _, stderr } => {
    517                assert!(stderr.contains("some unknown error"));
    518            }
    519            _ => panic!("expected PacmanKey error"),
    520        }
    521    }
    522}