1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-22 20:39:29 -07:00

More lenient key querying, smaller release binaries, don't panic on stdout close, formatting

The key querying first tries exact match on a fingerprint, and then falls back to contains() on the alias
This commit is contained in:
TymanWasTaken 2023-07-02 19:24:04 -04:00
parent 57e73d62dd
commit 954d6fac2e
Signed by: Ty
GPG key ID: 2813440C772555A4
6 changed files with 141 additions and 53 deletions

View file

@ -6,6 +6,7 @@
"josekit", "josekit",
"PKCS", "PKCS",
"Pkey", "Pkey",
"printdoc" "printdoc",
"writedoc"
] ]
} }

View file

@ -29,3 +29,7 @@ async-trait = "0.1.68"
[profile.release] [profile.release]
strip = true strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

View file

@ -105,7 +105,8 @@ impl AspKey {
}) })
} }
} }
})().or(Err(AspKeyError::GenerationError)) })()
.or(Err(AspKeyError::GenerationError))
} }
pub fn create_signer(&self) -> anyhow::Result<Box<dyn JwsSigner>> { pub fn create_signer(&self) -> anyhow::Result<Box<dyn JwsSigner>> {

View file

@ -1,12 +1,18 @@
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use asp::keys::AspKey; use asp::keys::AspKey;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use data_encoding::BASE64_NOPAD; use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Password}; use dialoguer::{theme::ColorfulTheme, Password};
use sea_orm::EntityTrait; use indoc::writedoc;
use crate::{commands::AspmSubcommand, entities::keys}; use std::io::Write;
use crate::{
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
entities::keys::{Entity as KeysEntity, Model as KeysModel},
};
#[derive(ValueEnum, Debug, Clone)] #[derive(ValueEnum, Debug, Clone)]
pub enum KeyExportFormat { pub enum KeyExportFormat {
@ -25,58 +31,86 @@ pub struct KeysExportCommand {
/// The format to export the key into. All of these formats can then be correctly imported back into this tool, but only some are encrypted, so keep that in mind when handling the exported keys. /// The format to export the key into. All of these formats can then be correctly imported back into this tool, but only some are encrypted, so keep that in mind when handling the exported keys.
#[clap(value_enum, ignore_case = true)] #[clap(value_enum, ignore_case = true)]
format: KeyExportFormat, format: KeyExportFormat,
/// The fingerprint of the key to export. This can be obtained with the `keys list` command. /// The key to export. This can either be a fingerprint obtained with the `keys list` command, or an alias to search for.
fingerprint: String, key: String,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl AspmSubcommand for KeysExportCommand { impl AspmSubcommand for KeysExportCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> { async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
// Fetch key from db // Fetch key from db
let entry = keys::Entity::find_by_id(&self.fingerprint) let entry = KeysEntity::query_key(&state.db, &self.key)
.one(&state.db)
.await .await
.context("Unable to fetch key from database")?; .context("Unable to query keys from database")?;
let key: KeysModel = match entry {
if let Some(entry) = entry { KeysQueryResult::None => {
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| { eprintln!(
Password::with_theme(&ColorfulTheme::default()) "{style}No keys matching the given query were found{reset}",
.with_prompt("Please enter a password to decrypt the key with") style = Anstyle::new()
.interact() .fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightRed)))
.context("Unable to prompt on stderr") .render(),
})?; reset = Reset.render()
);
let argon_salt = SaltString::from_b64( std::process::exit(1);
&BASE64_NOPAD.encode(self.fingerprint.to_uppercase().as_bytes()),
)
.context("Unable to decode argon2 salt")?;
let argon2 = Argon2::default();
let hash = argon2
.hash_password(key_password.as_bytes(), &argon_salt)
.or(Err(anyhow!("Unable to derive encryption key")))?;
let aes_key = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32];
if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &entry.encrypted) {
let export = match self.format {
KeyExportFormat::Encrypted => decrypted
.export_encrypted(aes_key)
.context("Unable to convert key into encrypted format")?,
KeyExportFormat::PKCS8 => decrypted
.into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?,
KeyExportFormat::Jwk => decrypted.jwk.to_string(),
};
println!("{export}")
} else {
eprintln!("There was an error decrypting the key, please make sure the password you entered was correct");
} }
} else { KeysQueryResult::One(key) => key,
KeysQueryResult::Many(mut keys) => {
eprintln!(
"{style}More than one keys matching the given query were found{reset}",
style = Anstyle::new()
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightYellow)))
.render(),
reset = Reset::default().render()
);
keys.remove(0)
}
};
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
Password::with_theme(&ColorfulTheme::default())
.with_prompt("Please enter a password to decrypt the key with")
.interact()
.context("Unable to prompt on stderr")
})?;
let argon_salt =
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes()))
.context("Unable to decode argon2 salt")?;
let argon2 = Argon2::default();
let hash = argon2
.hash_password(key_password.as_bytes(), &argon_salt)
.or(Err(anyhow!("Unable to derive encryption key")))?;
let aes_key = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32];
if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &key.encrypted) {
let export = match self.format {
KeyExportFormat::Encrypted => decrypted
.export_encrypted(aes_key)
.context("Unable to convert key into encrypted format")?,
KeyExportFormat::PKCS8 => decrypted
.into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?,
KeyExportFormat::Jwk => decrypted.jwk.to_string(),
};
eprintln!( eprintln!(
"The specified key fingerprint {fingerprint} does not exist", "{style}Exported key \"{alias}\" with fingerprint {fingerprint}:{reset}",
fingerprint = self.fingerprint style = Anstyle::new()
) .bold()
.underline()
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightMagenta)))
.render(),
alias = key.alias,
fingerprint = decrypted.fingerprint,
reset = Reset.render()
);
let _ = writedoc!(
std::io::stdout(),
"{export}"
);
} else {
eprintln!("There was an error decrypting the key, please make sure the password you entered was correct");
} }
Ok(()) Ok(())

View file

@ -2,9 +2,11 @@ use anstyle::{AnsiColor, Reset, Style as Anstyle};
use anyhow::Context; use anyhow::Context;
use asp::keys::AspKeyType; use asp::keys::AspKeyType;
use clap::Parser; use clap::Parser;
use indoc::printdoc; use indoc::{writedoc};
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use std::io::Write;
use crate::{commands::AspmSubcommand, entities::keys}; use crate::{commands::AspmSubcommand, entities::keys};
/// A command to list all saved keys, along with their fingerprints and types /// A command to list all saved keys, along with their fingerprints and types
@ -38,21 +40,23 @@ impl AspmSubcommand for KeysListCommand {
.render(); .render();
// Print output // Print output
println!( let _ = writedoc!(
"{header_style}Saved keys ({n} total):{reset}\n", std::io::stdout(),
"{header_style}Saved keys ({n} total):{reset}\n\n",
n = entries.len(), n = entries.len(),
); );
for entry in entries.iter() { for entry in entries.iter() {
printdoc! { let _ = writedoc! {
std::io::stdout(),
" "
{alias_style}{alias}:{reset} {alias_style}{alias}:{reset}
{key_style}Fingerprint{reset} {value_style}{fingerprint}{reset} {key_style}Fingerprint{reset} {value_style}{fingerprint}{reset}
{key_style}Key Type{reset} {value_style}{key_type:?}{reset} {key_style}Key Type{reset} {value_style}{key_type:?}{reset}\n
", ",
fingerprint = entry.fingerprint, fingerprint = entry.fingerprint,
key_type = TryInto::<AspKeyType>::try_into(entry.key_type).context("Unable to get key type from database")?, key_type = TryInto::<AspKeyType>::try_into(entry.key_type).context("Unable to get key type from database")?,
alias = entry.alias alias = entry.alias
} };
} }
Ok(()) Ok(())

View file

@ -1,10 +1,54 @@
pub mod keys; pub mod keys;
use clap::Parser; use clap::Parser;
use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
use crate::entities::keys::{Column as KeysColumn, Entity as KeysEntity, Model as KeysModel};
use crate::AspmState; use crate::AspmState;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait AspmSubcommand: Parser { pub trait AspmSubcommand: Parser {
async fn execute(&self, state: AspmState) -> Result<(), anyhow::Error>; async fn execute(&self, state: AspmState) -> Result<(), anyhow::Error>;
} }
#[async_trait::async_trait]
pub(self) trait KeysEntityExt {
type ResultEnum;
/// Queries the database for a specific key entity, first checking by fingerprint and then checking
async fn query_key(db: &DatabaseConnection, query: &str) -> anyhow::Result<Self::ResultEnum>;
}
pub(self) enum KeysQueryResult {
None,
One(KeysModel),
Many(Vec<KeysModel>),
}
#[async_trait::async_trait]
impl KeysEntityExt for KeysEntity {
type ResultEnum = KeysQueryResult;
async fn query_key(db: &DatabaseConnection, query: &str) -> anyhow::Result<Self::ResultEnum> {
let mut models = KeysEntity::find()
.filter(
Condition::any()
.add(KeysColumn::Fingerprint.eq(query))
.add(KeysColumn::Alias.contains(query)),
)
.order_by(
// This order_by will assign a higher order priority if the fingerprint is EXACTLY the query, and everything else gets lower
// This effectively means that if there was an exact fingerprint match, it is ordered first, and any LIKE `%alias%` matches come after
KeysColumn::Fingerprint,
sea_orm::Order::Field(sea_orm::Values(vec![query.into()])),
)
.all(db)
.await?;
Ok(match models.len() {
0 => Self::ResultEnum::None,
1 => Self::ResultEnum::One(models.remove(0)),
_ => Self::ResultEnum::Many(models),
})
}
}