Skip to main content

ssh_key/
dot_ssh.rs

1//! `~/.ssh` support.
2
3use crate::{Fingerprint, PrivateKey, PublicKey, Result};
4use core::fmt::{self, Debug};
5use std::{
6    env,
7    fs::{self, ReadDir},
8    path::{Path, PathBuf},
9};
10
11#[cfg(doc)]
12use crate::Error;
13
14/// `~/.ssh` directory support (or similarly structured directories).
15#[derive(Clone, Eq, PartialEq)]
16pub struct DotSsh {
17    path: PathBuf,
18}
19
20impl DotSsh {
21    /// Open `~/.ssh` if the home directory can be located.
22    ///
23    /// Returns `None` if the home directory couldn't be located.
24    #[must_use]
25    pub fn new() -> Option<Self> {
26        #[allow(deprecated, reason = "TODO MSRV: Rust 1.86 un-deprecates this")]
27        env::home_dir().map(|path| Self::open(path.join(".ssh")))
28    }
29
30    /// Open a `~/.ssh`-structured directory.
31    ///
32    /// Does not verify that the directory exists or has the right file permissions.
33    ///
34    /// Attempts to canonicalize the path once opened.
35    pub fn open(path: impl Into<PathBuf>) -> Self {
36        let path = path.into();
37        Self {
38            path: path.canonicalize().unwrap_or(path),
39        }
40    }
41
42    /// Get the path to the `~/.ssh` directory (or whatever [`DotSsh::open`] was called with).
43    #[must_use]
44    pub fn path(&self) -> &Path {
45        &self.path
46    }
47
48    /// Get the path to the `~/.ssh/config` configuration file. Does not check if it exists.
49    #[must_use]
50    pub fn config_path(&self) -> PathBuf {
51        self.path.join("config")
52    }
53
54    /// Iterate over the private keys in the `~/.ssh` directory.
55    ///
56    /// # Errors
57    /// Returns [`Error::Io`] in the event of I/O errors.
58    pub fn private_keys(&self) -> Result<impl Iterator<Item = PrivateKey>> {
59        Ok(PrivateKeysIter {
60            read_dir: fs::read_dir(&self.path)?,
61        })
62    }
63
64    /// Find a private key whose public key has the given key fingerprint.
65    #[must_use]
66    pub fn private_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option<PrivateKey> {
67        self.private_keys()
68            .ok()?
69            .find(|key| key.public_key().fingerprint(fingerprint.algorithm()) == fingerprint)
70    }
71
72    /// Iterate over the public keys in the `~/.ssh` directory.
73    ///
74    /// # Errors
75    /// Returns [`Error::Io`] in the event of I/O errors.
76    pub fn public_keys(&self) -> Result<impl Iterator<Item = PublicKey>> {
77        Ok(PublicKeysIter {
78            read_dir: fs::read_dir(&self.path)?,
79        })
80    }
81
82    /// Find a public key with the given key fingerprint.
83    #[must_use]
84    pub fn public_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option<PublicKey> {
85        self.public_keys()
86            .ok()?
87            .find(|key| key.fingerprint(fingerprint.algorithm()) == fingerprint)
88    }
89
90    /// Write a private key into `~/.ssh`.
91    ///
92    /// # Errors
93    /// Returns [`Error::Io`] in the event of I/O errors.
94    pub fn write_private_key(&self, filename: impl AsRef<Path>, key: &PrivateKey) -> Result<()> {
95        key.write_openssh_file(self.path.join(filename), Default::default())
96    }
97
98    /// Write a public key into `~/.ssh`.
99    ///
100    /// # Errors
101    /// Returns [`Error::Io`] in the event of I/O errors.
102    pub fn write_public_key(&self, filename: impl AsRef<Path>, key: &PublicKey) -> Result<()> {
103        key.write_openssh_file(self.path.join(filename))
104    }
105}
106
107impl Debug for DotSsh {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("DotSsh").finish_non_exhaustive()
110    }
111}
112
113impl Default for DotSsh {
114    /// Calls [`DotSsh::new`] and panics if the home directory could not be located.
115    fn default() -> Self {
116        Self::new().expect("home directory could not be located")
117    }
118}
119
120/// Iterator over the private keys in the `~/.ssh` directory.
121pub(crate) struct PrivateKeysIter {
122    read_dir: ReadDir,
123}
124
125impl Iterator for PrivateKeysIter {
126    type Item = PrivateKey;
127
128    fn next(&mut self) -> Option<Self::Item> {
129        loop {
130            let entry = self.read_dir.next()?.ok()?;
131
132            if let Ok(key) = PrivateKey::read_openssh_file(entry.path()) {
133                return Some(key);
134            }
135        }
136    }
137}
138
139/// Iterator over the public keys in the `~/.ssh` directory.
140pub(crate) struct PublicKeysIter {
141    read_dir: ReadDir,
142}
143
144impl Iterator for PublicKeysIter {
145    type Item = PublicKey;
146
147    fn next(&mut self) -> Option<Self::Item> {
148        loop {
149            let entry = self.read_dir.next()?.ok()?;
150
151            if let Ok(key) = PublicKey::read_openssh_file(entry.path()) {
152                return Some(key);
153            }
154        }
155    }
156}