use std::ffi::OsStr;
use std::fs::File;
use std::fs::read_dir;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context as _;
use anyhow::Result;
use gpgme::Context;
use gpgme::Protocol;
const PUBLIC_EXT: &str = "pub";
const PRIVATE_EXT: &str = "gpg";
#[derive(Debug)]
pub struct PemPublicKey(Vec<u8>);
impl From<PemPublicKey> for Vec<u8> {
fn from(key: PemPublicKey) -> Self {
key.0
}
}
#[derive(Debug)]
pub struct PemPrivateKey(Vec<u8>);
impl From<PemPrivateKey> for Vec<u8> {
fn from(key: PemPrivateKey) -> Self {
key.0
}
}
pub fn load_private_key(file: &Path) -> Result<PemPrivateKey> {
let mut input =
File::open(file).with_context(|| format!("failed to open {} for reading", file.display()))?;
let mut gpg =
Context::from_protocol(Protocol::OpenPgp).with_context(|| "failed to connect to GPG")?;
let mut output = Vec::new();
let _ = gpg
.decrypt(&mut input, &mut output)
.with_context(|| format!("failed to decrypt {}", file.display()))?;
Ok(PemPrivateKey(output))
}
pub(crate) fn load_public_key<P>(file: P) -> Result<PemPublicKey>
where
P: AsRef<Path>,
{
let file = file.as_ref();
let mut f =
File::open(file).with_context(|| format!("failed to open {} for reading", file.display()))?;
let mut data = Vec::new();
let _ = f
.read_to_end(&mut data)
.with_context(|| format!("failed to read data from {}", file.display()))?;
Ok(PemPublicKey(data))
}
pub fn public_keys<P>(dir: P) -> Result<impl Iterator<Item = Result<(PemPublicKey, PathBuf)>>>
where
P: Into<PathBuf>,
{
let dir = dir.into();
read_dir(&dir)
.with_context(|| format!("failed to read contents of {}", dir.display()))
.map(move |x| {
x.filter_map(move |entry| match entry {
Ok(entry) => {
let path = entry.path();
if path.exists() && !path.is_dir() && path.extension() == Some(OsStr::new(PUBLIC_EXT)) {
let mut gpg_path = path.clone();
let _ = gpg_path.set_extension(OsStr::new(PRIVATE_EXT));
if gpg_path.exists() && !gpg_path.is_dir() {
Some(load_public_key(&path).map(|x| (x, gpg_path)))
} else {
None
}
} else {
None
}
}
Err(err) => Some(Err(err).with_context(|| {
format!(
"failed to read directory entry in {}",
dir.display(),
)
})),
})
})
}
#[cfg(test)]
pub mod test {
use super::*;
use crate::keys::FromPem;
use ssh_agent_lib::proto::private_key::PrivateKey;
use ssh_agent_lib::proto::public_key::PublicKey;
pub fn load_unencrypted_private_key<P>(file: P) -> Result<PemPrivateKey>
where
P: AsRef<Path>,
{
let file = file.as_ref();
let mut input =
File::open(file).with_context(|| format!("failed to open {} for reading", file.display()))?;
let mut output = Vec::new();
let _ = input
.read_to_end(&mut output)
.with_context(|| format!("failed to read data from {}", file.display()))?;
Ok(PemPrivateKey(output))
}
#[test]
fn load_public_keys() -> Result<()> {
let mut keys = public_keys("tests/valid_keys")?;
let (_, path) = keys.next().unwrap()?;
assert_eq!(path.to_str().unwrap(), "tests/valid_keys/ed25519.gpg");
let (_, path) = keys.next().unwrap()?;
assert_eq!(path.to_str().unwrap(), "tests/valid_keys/rsa2048.gpg");
assert!(keys.next().is_none());
Ok(())
}
#[test]
fn dont_load_invalid_public_keys() -> Result<()> {
let keys = public_keys("tests/invalid_keys")?;
assert_eq!(keys.count(), 0);
Ok(())
}
#[test]
fn public_key_conversion_ed25519() -> Result<()> {
let pubkey = load_public_key("tests/valid_keys/ed25519.pub")?;
let _ = PublicKey::from_pem(pubkey)?;
Ok(())
}
#[test]
fn private_key_conversion_ed25519() -> Result<()> {
let privkey = load_unencrypted_private_key("tests/valid_keys/ed25519")?;
let _ = PrivateKey::from_pem(privkey)?;
Ok(())
}
#[test]
fn public_key_conversion_rsa2048() -> Result<()> {
let pubkey = load_public_key("tests/valid_keys/rsa2048.pub")?;
let _ = PublicKey::from_pem(pubkey)?;
Ok(())
}
#[test]
fn private_key_conversion_rsa2048() -> Result<()> {
let privkey = load_unencrypted_private_key("tests/valid_keys/rsa2048")?;
let _ = PrivateKey::from_pem(privkey)?;
Ok(())
}
}