diff --git a/Cargo.lock b/Cargo.lock index abc16c8..484bda7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + [[package]] name = "hybrid-array" version = "0.2.0-rc.9" @@ -816,6 +825,7 @@ dependencies = [ "dsa", "ed25519-dalek", "hex-literal", + "home", "num-bigint-dig", "p256", "p384", @@ -888,6 +898,79 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zeroize" version = "1.8.1" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 8897698..a45b521 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -30,6 +30,7 @@ bcrypt-pbkdf = { version = "=0.11.0-pre.1", optional = true, default-features = bigint = { package = "num-bigint-dig", version = "0.8", optional = true, default-features = false } dsa = { version = "=0.7.0-pre.0", optional = true, default-features = false } ed25519-dalek = { version = "=2.2.0-pre", optional = true, default-features = false } +home = { version = "0.5", optional = true } p256 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } p384 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } p521 = { version = "=0.14.0-pre.1", optional = true, default-features = false, features = ["ecdsa"] } @@ -58,7 +59,8 @@ std = [ "p521?/std", "rsa?/std", "sec1?/std", - "signature/std" + "signature/std", + "dep:home" ] crypto = ["ed25519", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak diff --git a/ssh-key/src/dot_ssh.rs b/ssh-key/src/dot_ssh.rs new file mode 100644 index 0000000..5d935f2 --- /dev/null +++ b/ssh-key/src/dot_ssh.rs @@ -0,0 +1,117 @@ +//! `~/.ssh` support. + +use crate::{Fingerprint, PrivateKey, PublicKey, Result}; +use std::{ + fs::{self, ReadDir}, + path::{Path, PathBuf}, +}; + +/// `~/.ssh` directory support (or similarly structured directories). +#[derive(Clone, Eq, PartialEq)] +pub struct DotSsh { + path: PathBuf, +} + +impl DotSsh { + /// Open `~/.ssh` if the home directory can be located. + /// + /// Returns `None` if the home directory couldn't be located. + pub fn new() -> Option { + home::home_dir().map(|path| Self::open(path.join(".ssh"))) + } + + /// Open a `~/.ssh`-structured directory. + /// + /// Does not verify that the directory exists or has the right file permissions. + /// + /// Attempts to canonicalize the path once opened. + pub fn open(path: impl Into) -> Self { + let path = path.into(); + Self { + path: path.canonicalize().unwrap_or(path), + } + } + + /// Get the path to the `~/.ssh` directory (or whatever [`DotSsh::open`] was called with). + pub fn path(&self) -> &Path { + &self.path + } + + /// Get the path to the `~/.ssh/config` configuration file. Does not check if it exists. + pub fn config_path(&self) -> PathBuf { + self.path.join("config") + } + + /// Iterate over the private keys in the `.ssh` directory. + pub fn private_keys(&self) -> Result> { + Ok(PrivateKeysIter { + read_dir: fs::read_dir(&self.path)?, + }) + } + + /// Find a private key whose public key has the given key fingerprint. + pub fn private_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option { + self.private_keys() + .ok()? + .find(|key| key.public_key().fingerprint(fingerprint.algorithm()) == fingerprint) + } + + /// Iterate over the public keys in the `.ssh` directory. + pub fn public_keys(&self) -> Result> { + Ok(PublicKeysIter { + read_dir: fs::read_dir(&self.path)?, + }) + } + + /// Find a public key with the given key fingerprint. + pub fn public_key_with_fingerprint(&self, fingerprint: Fingerprint) -> Option { + self.public_keys() + .ok()? + .find(|key| key.fingerprint(fingerprint.algorithm()) == fingerprint) + } +} + +impl Default for DotSsh { + /// Calls [`DotSsh::new`] and panics if the home directory could not be located. + fn default() -> Self { + Self::new().expect("home directory could not be located") + } +} + +/// Iterator over the private keys in the `.ssh` directory. +pub struct PrivateKeysIter { + read_dir: ReadDir, +} + +impl Iterator for PrivateKeysIter { + type Item = PrivateKey; + + fn next(&mut self) -> Option { + loop { + let entry = self.read_dir.next()?.ok()?; + + if let Ok(key) = PrivateKey::read_openssh_file(&entry.path()) { + return Some(key); + } + } + } +} + +/// Iterator over the public keys in the `.ssh` directory. +pub struct PublicKeysIter { + read_dir: ReadDir, +} + +impl Iterator for PublicKeysIter { + type Item = PublicKey; + + fn next(&mut self) -> Option { + loop { + let entry = self.read_dir.next()?.ok()?; + + if let Ok(key) = PublicKey::read_openssh_file(&entry.path()) { + return Some(key); + } + } + } +} diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 2b6eb46..56acda8 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -156,6 +156,8 @@ mod error; mod fingerprint; mod kdf; +#[cfg(feature = "std")] +mod dot_ssh; #[cfg(feature = "alloc")] mod mpint; #[cfg(feature = "alloc")] @@ -191,3 +193,6 @@ pub use sec1; #[cfg(feature = "rand_core")] pub use rand_core; + +#[cfg(feature = "std")] +pub use crate::dot_ssh::DotSsh; diff --git a/ssh-key/tests/dot_ssh.rs b/ssh-key/tests/dot_ssh.rs new file mode 100644 index 0000000..e5ce090 --- /dev/null +++ b/ssh-key/tests/dot_ssh.rs @@ -0,0 +1,52 @@ +//! Tests for `~/.ssh` support. Uses the `tests/examples` directory instead. + +#![cfg(feature = "std")] + +use hex_literal::hex; +use ssh_key::{Algorithm, DotSsh, Fingerprint}; + +/// Open `.ssh` using the `test/examples`. +fn dot_ssh() -> DotSsh { + DotSsh::open("tests/examples") +} + +#[test] +fn path_round_trip() { + let dot_ssh = dot_ssh(); + dbg!(dot_ssh.path()); + assert!(dot_ssh.path().ends_with("tests/examples")); +} + +#[test] +fn private_keys() { + let dot_ssh = dot_ssh(); + assert_eq!(dot_ssh.private_keys().unwrap().count(), 20); +} + +#[test] +fn private_key_with_fingerprint() { + let fingerprint = Fingerprint::Sha256(hex!( + "5025222ebecf8ecf7014524c0c1c8b81cdcdaed754df8e0e814338e7064f7084" + )); + + let dot_ssh = dot_ssh(); + let key = dot_ssh.private_key_with_fingerprint(fingerprint).unwrap(); + assert_eq!(key.algorithm(), Algorithm::Ed25519); +} + +#[test] +fn public_keys() { + let dot_ssh = dot_ssh(); + assert_eq!(dot_ssh.public_keys().unwrap().count(), 12); +} + +#[test] +fn public_key_with_fingerprint() { + let fingerprint = Fingerprint::Sha256(hex!( + "5025222ebecf8ecf7014524c0c1c8b81cdcdaed754df8e0e814338e7064f7084" + )); + + let dot_ssh = dot_ssh(); + let key = dot_ssh.public_key_with_fingerprint(fingerprint).unwrap(); + assert_eq!(key.algorithm(), Algorithm::Ed25519); +}