mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 19:29:28 -07:00
I had this sitting on my computer for months and to be honest no clue what i did
This commit is contained in:
parent
ecbb551e34
commit
cf6bbfeecf
6 changed files with 129 additions and 119 deletions
|
@ -23,7 +23,7 @@ use crate::{
|
||||||
entities::{prelude::*, profiles},
|
entities::{prelude::*, profiles},
|
||||||
};
|
};
|
||||||
|
|
||||||
///
|
/// Upload an existing profile to an ASPE server
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub struct AspeCreateCommand {
|
pub struct AspeCreateCommand {
|
||||||
/// The fingerprint or alias of the profile to upload
|
/// The fingerprint or alias of the profile to upload
|
||||||
|
@ -130,7 +130,10 @@ impl AspmSubcommand for AspeCreateCommand {
|
||||||
.context("Unable to encode the profile as a JWT and sign it")?;
|
.context("Unable to encode the profile as a JWT and sign it")?;
|
||||||
|
|
||||||
match server.post_request(encoded_request).await {
|
match server.post_request(encoded_request).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => {
|
||||||
|
println!("Successfully uploaded profile!");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
Err(AspeRequestFailure::BadRequest) => bail!("The ASPE server rejected the request due to invalid data"),
|
Err(AspeRequestFailure::BadRequest) => bail!("The ASPE server rejected the request due to invalid data"),
|
||||||
Err(AspeRequestFailure::TooLarge) => bail!("The ASPE server rejected the request as being too large"),
|
Err(AspeRequestFailure::TooLarge) => bail!("The ASPE server rejected the request as being too large"),
|
||||||
Err(AspeRequestFailure::RateLimited) => bail!("The ASPE server rejected the request due to a ratelimit"),
|
Err(AspeRequestFailure::RateLimited) => bail!("The ASPE server rejected the request due to a ratelimit"),
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
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 dialoguer::{console::Term, theme::ColorfulTheme, Confirm};
|
use dialoguer::{console::Term, theme::ColorfulTheme, Confirm};
|
||||||
use indoc::writedoc;
|
use indoc::writedoc;
|
||||||
use sea_orm::ModelTrait;
|
use sea_orm::ModelTrait as _;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
commands::AspmSubcommand,
|
||||||
entities::prelude::*,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deletes a saved key, after asking for confirmation.
|
/// Deletes a saved key, after asking for confirmation.
|
||||||
|
@ -27,32 +27,7 @@ pub struct KeysDeleteCommand {
|
||||||
impl AspmSubcommand for KeysDeleteCommand {
|
impl AspmSubcommand for KeysDeleteCommand {
|
||||||
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::query_key(&state.db, &self.key)
|
let key = utils::keys::query_key(&state.db, self.key).await?;
|
||||||
.await
|
|
||||||
.context("Unable to query keys from database")?;
|
|
||||||
let key = match entry {
|
|
||||||
KeysQueryResult::None => {
|
|
||||||
eprintln!(
|
|
||||||
"{style}No keys matching the given query were found{reset}",
|
|
||||||
style = Anstyle::new()
|
|
||||||
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightRed)))
|
|
||||||
.render(),
|
|
||||||
reset = Reset.render()
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
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.render()
|
|
||||||
);
|
|
||||||
keys.remove(0)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.no_confirm {
|
if !self.no_confirm {
|
||||||
// Construct styles
|
// Construct styles
|
||||||
|
|
|
@ -3,8 +3,7 @@ use aes_gcm::{
|
||||||
Aes256Gcm, Key,
|
Aes256Gcm, Key,
|
||||||
};
|
};
|
||||||
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::Context as _;
|
||||||
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, BASE64_NOPAD};
|
use data_encoding::{BASE64, BASE64_NOPAD};
|
||||||
|
@ -12,10 +11,7 @@ use dialoguer::{theme::ColorfulTheme, Password};
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{commands::AspmSubcommand, utils};
|
||||||
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
|
||||||
entities::prelude::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(tag = "alg", rename = "scrypt")]
|
#[serde(tag = "alg", rename = "scrypt")]
|
||||||
|
@ -63,32 +59,7 @@ pub struct KeysExportCommand {
|
||||||
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::query_key(&state.db, &self.key)
|
let key = utils::keys::query_key(&state.db, self.key).await?;
|
||||||
.await
|
|
||||||
.context("Unable to query keys from database")?;
|
|
||||||
let key = match entry {
|
|
||||||
KeysQueryResult::None => {
|
|
||||||
eprintln!(
|
|
||||||
"{style}No keys matching the given query were found{reset}",
|
|
||||||
style = Anstyle::new()
|
|
||||||
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightRed)))
|
|
||||||
.render(),
|
|
||||||
reset = Reset.render()
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
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.render()
|
|
||||||
);
|
|
||||||
keys.remove(0)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
|
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
|
||||||
Password::with_theme(&ColorfulTheme::default())
|
Password::with_theme(&ColorfulTheme::default())
|
||||||
|
@ -97,18 +68,10 @@ impl AspmSubcommand for KeysExportCommand {
|
||||||
.context("Unable to prompt on stderr")
|
.context("Unable to prompt on stderr")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let argon_salt =
|
let derived_key = utils::keys::derive_encryption_key(key.fingerprint, &key_password)?;
|
||||||
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) = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(aes_key))
|
if let Ok(decrypted) = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&derived_key))
|
||||||
.decrypt((&aes_key[0..12]).into(), key.cipher_text.as_slice())
|
.decrypt((&derived_key[0..12]).into(), key.cipher_text.as_slice())
|
||||||
{
|
{
|
||||||
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
|
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,8 @@ pub mod profiles;
|
||||||
pub mod aspe;
|
pub mod aspe;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
|
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
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]
|
||||||
|
@ -18,45 +16,3 @@ pub trait AspmSubcommand: Parser + Sync + Send {
|
||||||
runtime.block_on(self.execute(state))
|
runtime.block_on(self.execute(state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod commands;
|
mod commands;
|
||||||
#[allow(warnings)] // This is autogenerated, no use in showing warnings
|
#[allow(warnings)] // This is autogenerated, no use in showing warnings
|
||||||
mod entities;
|
mod entities;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle};
|
use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
112
src/utils.rs
Normal file
112
src/utils.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
pub mod keys {
|
||||||
|
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
||||||
|
use anyhow::{anyhow, Context as _};
|
||||||
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
|
use data_encoding::BASE64_NOPAD;
|
||||||
|
use sea_orm::{
|
||||||
|
ColumnTrait as _, Condition, DatabaseConnection, EntityTrait as _, QueryFilter as _,
|
||||||
|
QueryOrder as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::entities::{
|
||||||
|
keys::{Column as KeysColumn, Entity as KeysEntity, Model as KeysModel},
|
||||||
|
prelude::Keys,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn derive_encryption_key<S: AsRef<str>, P: AsRef<[u8]>>(
|
||||||
|
fingerprint: S,
|
||||||
|
password: P,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let argon_salt =
|
||||||
|
SaltString::from_b64(&BASE64_NOPAD.encode(fingerprint.as_ref().to_uppercase().as_bytes()))
|
||||||
|
.context("Unable to derive argon2 salt")?;
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let hash = argon2
|
||||||
|
.hash_password(password.as_ref(), &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];
|
||||||
|
|
||||||
|
Ok(aes_key.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
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 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_key<S: AsRef<str>>(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
query: S,
|
||||||
|
) -> anyhow::Result<KeysModel> {
|
||||||
|
let entry = Keys::query_key(db, query.as_ref())
|
||||||
|
.await
|
||||||
|
.context("Unable to query keys from database")?;
|
||||||
|
match entry {
|
||||||
|
KeysQueryResult::None => {
|
||||||
|
eprintln!(
|
||||||
|
"{style}No keys matching the given query were found{reset}",
|
||||||
|
style = Anstyle::new()
|
||||||
|
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightRed)))
|
||||||
|
.render(),
|
||||||
|
reset = Reset.render()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
KeysQueryResult::One(key) => Ok(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.render()
|
||||||
|
);
|
||||||
|
Ok(keys.remove(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue