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:
parent
57e73d62dd
commit
954d6fac2e
6 changed files with 141 additions and 53 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -6,6 +6,7 @@
|
||||||
"josekit",
|
"josekit",
|
||||||
"PKCS",
|
"PKCS",
|
||||||
"Pkey",
|
"Pkey",
|
||||||
"printdoc"
|
"printdoc",
|
||||||
|
"writedoc"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue