use core::fmt;
use std::{
collections::{
HashMap,
HashSet,
},
fs,
io::Write,
path::{
Path,
PathBuf,
},
};
use pgp::{
types::KeyTrait,
Deserializable,
};
use yapma_common::http;
pub use ssh_key as ssh;
pub mod gpg {
pub use pgp::{
composed::signed_key::SignedPublicKey,
types::KeyTrait,
Deserializable,
};
pub type Fingerprint = Vec<u8>;
}
mod github;
use github::Github;
mod gitlab;
use gitlab::Gitlab;
mod resolver;
use resolver::Resolver as _;
use tracing::debug;
pub type Error = anyhow::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Platform {
Github,
Gitlab,
}
impl Platform {
fn as_path(&self) -> &Path {
match self {
Self::Github => Path::new("github.com"),
Self::Gitlab => Path::new("gitlab.com"),
}
}
}
#[derive(Clone, PartialEq)]
pub struct Identity {
pub platform: Platform,
pub username: String,
pub ssh: HashSet<ssh::PublicKey>,
pub gpg: HashMap<gpg::Fingerprint, gpg::SignedPublicKey>,
}
impl fmt::Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity")
.field("platform", &self.platform)
.field("username", &self.username)
.field(
"ssh",
&self
.ssh
.iter()
.map(|key| key.to_openssh().unwrap())
.collect::<Vec<_>>(),
)
.field(
"gpg",
&self.gpg.keys().map(hex::encode_upper).collect::<Vec<_>>(),
)
.finish()
}
}
impl Identity {
pub async fn resolve<S, C>(platform: Platform, username: S, client: &C) -> Result<Self>
where
S: AsRef<str> + Into<String>,
C: http::Client,
{
let resolver::Keys { ssh, gpg } = match platform {
Platform::Github => Github::new(username.as_ref()).resolve(client).await?,
Platform::Gitlab => Gitlab::new(username.as_ref()).resolve(client).await?,
};
Ok(Self {
platform,
username: username.into(),
ssh,
gpg,
})
}
#[tracing::instrument(skip(root))]
pub fn load(
root: impl Into<PathBuf>,
platform: Platform,
username: String,
) -> Result<Option<Self>> {
let mut root: PathBuf = root.into();
root.push("identities");
root.push(platform.as_path());
root.push(&username);
if !root.try_exists()? {
return Ok(None);
}
let mut ssh = HashSet::new();
let mut gpg = HashMap::new();
debug!("Loading keys from {}", root.display());
for entry in fs::read_dir(&root)? {
let entry = entry?;
let path = entry.path();
match path.extension().and_then(|os| os.to_str()) {
Some("ssh") => {
let key = ssh::PublicKey::read_openssh_file(&path)?;
debug!("Read SSH key from {}", path.display());
ssh.insert(key);
},
Some("gpg") => {
let ascii = fs::read_to_string(&path)?;
let (key, _) = gpg::SignedPublicKey::from_string(&ascii)?;
debug!("Read GPG key from {}", path.display());
gpg.insert(key.fingerprint(), key);
},
_ => {},
}
}
Ok(Some(Self {
platform,
username,
ssh,
gpg,
}))
}
#[tracing::instrument(skip_all, fields(platform = ?self.platform, username = self.username))]
pub fn store(&self, root: impl Into<PathBuf>) -> Result<()> {
let mut root: PathBuf = root.into();
root.push("identities");
root.push(self.platform.as_path());
root.push(&self.username);
fs::create_dir_all(&root)?;
for key in &self.ssh {
let fp = key.fingerprint(ssh::HashAlg::Sha512);
root.push(bs58::encode(fp.as_bytes()).into_string());
root.set_extension("tmp");
let mut file = fs::File::options()
.create_new(true)
.write(true)
.open(&root)?;
let key = key.to_openssh()?;
file.write_all(key.as_bytes())?;
file.sync_all()?;
drop(file);
let target = root.with_extension("ssh");
fs::rename(&root, &target)?;
debug!("Wrote SSH key to {}", target.display());
root.pop();
}
for (fp, key) in &self.gpg {
root.push(hex::encode_upper(fp));
root.set_extension("tmp");
let mut file = fs::File::options()
.create_new(true)
.write(true)
.open(&root)?;
key.to_armored_writer(&mut file, None)?;
file.sync_all()?;
drop(file);
let target = root.with_extension("gpg");
fs::rename(&root, &target)?;
debug!("Wrote GPG key to {}", target.display());
root.pop();
}
Ok(())
}
}