1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-23 01:19:28 -07:00

More work on profiles create, and profiles export was added

This commit is contained in:
Tyler Beckman 2024-02-28 21:06:10 -07:00
parent ac7785a43e
commit 86ffe01c4d
Signed by: Ty
GPG key ID: 2813440C772555A4
7 changed files with 182 additions and 6 deletions

View file

@ -24,6 +24,15 @@ pub enum AspKeyType {
ES256, ES256,
} }
impl std::fmt::Display for AspKeyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::ES256 => "P-256",
Self::Ed25519 => "Ed25519",
})
}
}
impl From<AspKeyType> for i32 { impl From<AspKeyType> for i32 {
fn from(value: AspKeyType) -> Self { fn from(value: AspKeyType) -> Self {
match value { match value {

View file

@ -4,7 +4,7 @@ pub struct Migration;
impl MigrationName for Migration { impl MigrationName for Migration {
fn name(&self) -> &str { fn name(&self) -> &str {
"m_20242801_000002_create_profiles_table.rs" "m_20242801_000002_create_profiles_table"
} }
} }
@ -58,6 +58,7 @@ impl MigrationTrait for Migration {
.on_update(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade), .on_delete(ForeignKeyAction::Cascade),
) )
.col(ColumnDef::new(Profiles::Alias).string().not_null())
.col(ColumnDef::new(Profiles::Name).string().not_null()) .col(ColumnDef::new(Profiles::Name).string().not_null())
.col(ColumnDef::new(Profiles::Description).string().null()) .col(ColumnDef::new(Profiles::Description).string().null())
.col(ColumnDef::new(Profiles::AvatarUrl).string().null()) .col(ColumnDef::new(Profiles::AvatarUrl).string().null())
@ -88,6 +89,7 @@ impl MigrationTrait for Migration {
pub enum Profiles { pub enum Profiles {
Table, Table,
Key, Key,
Alias,
Name, Name,
Description, Description,
AvatarUrl, AvatarUrl,

View file

@ -1,11 +1,19 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::Context; use anyhow::Context;
use asp::{profiles::*, utils::jwt::AspJwsType}; use asp::{keys::AspKeyType, profiles::*, utils::jwt::AspJwsType};
use clap::Parser; use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Input}; use dialoguer::{theme::ColorfulTheme, Input, Select};
use sea_orm::{ActiveValue, EntityTrait};
use crate::commands::AspmSubcommand; use crate::{
commands::AspmSubcommand,
entities::{
claims::{self, Entity as Claims},
keys::Entity as Keys,
profiles::{self, Entity as Profiles},
},
};
/// Creates a new profile containing claims and other metadata. /// Creates a new profile containing claims and other metadata.
/// A key is needed to attach this profile to, but multiple profiles can be created for the same key. /// A key is needed to attach this profile to, but multiple profiles can be created for the same key.
@ -32,6 +40,31 @@ impl AspmSubcommand for ProfilesCreateCommand {
.context("Unable to prompt on stderr")? .context("Unable to prompt on stderr")?
}; };
let keys = Keys::find()
.all(&state.db)
.await
.context("Unable to query keys from database")?;
let key_index = Select::with_theme(&theme)
.with_prompt("Please choose the key to attach this profile to")
.items(
keys.iter()
.map(|entry| {
Ok(format!(
"{alias} - {fingerprint} ({key_type})",
alias = entry.alias,
fingerprint = entry.fingerprint,
key_type = AspKeyType::try_from(entry.key_type)
.context("Unable to parse key type from database")?
))
})
.collect::<Result<Vec<String>, anyhow::Error>>()?
.as_slice(),
)
.default(0)
.interact()
.context("Unable to prompt for key on stderr")?;
let profile = AriadneSignatureProfile { let profile = AriadneSignatureProfile {
version: 0, version: 0,
r#type: AspJwsType::Profile, r#type: AspJwsType::Profile,
@ -85,7 +118,30 @@ impl AspmSubcommand for ProfilesCreateCommand {
.map_or(Ok(None), |hex: String| HexColor::from_str(&hex).context("Unable to parse color code").map(Some))?, .map_or(Ok(None), |hex: String| HexColor::from_str(&hex).context("Unable to parse color code").map(Some))?,
}; };
dbg!(profile); // Save the profile information to the database
Profiles::insert(profiles::ActiveModel {
alias: ActiveValue::Set(alias),
name: ActiveValue::Set(profile.name),
description: ActiveValue::Set(profile.description),
email: ActiveValue::Set(profile.email.map(|email| email.to_string())),
avatar_url: ActiveValue::Set(
profile.avatar_url.map(|avatar_url| avatar_url.to_string()),
),
color: ActiveValue::Set(profile.color.map(|color| color.to_string())),
key: ActiveValue::Set(keys[key_index].fingerprint.clone()),
})
.exec(&state.db)
.await
.context("Unable to insert profile into database")?;
// Add all of the claims and link them to the profile that was just created
Claims::insert_many(profile.claims.into_iter().map(|claim| claims::ActiveModel {
profile: ActiveValue::Set(keys[key_index].fingerprint.clone()),
uri: ActiveValue::Set(claim),
..Default::default()
}))
.exec(&state.db)
.await
.context("Unable to insert claims into database")?;
Ok(()) Ok(())
} }

View file

@ -0,0 +1,101 @@
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit as _};
use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _};
use asp::{
keys::AspKey,
profiles::{AriadneSignatureProfile, Email, HexColor, Url},
utils::jwt::{AspJwsType, JwtSerialize},
};
use clap::Parser;
use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Password};
use josekit::jwk::Jwk;
use sea_orm::{EntityTrait, ModelTrait};
use crate::{commands::AspmSubcommand, entities::prelude::*};
/// Exports a profile in JWT format
#[derive(Parser, Debug)]
pub struct ProfilesExportCommand {
/// The fingerprint of the profile to export
fingerprint: String,
}
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesExportCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let Some(profile) = Profiles::find_by_id(&self.fingerprint)
.one(&state.db)
.await
.context("Unable to query for profiles with fingerprint")?
else {
bail!("No profile found with that fingerprint");
};
let claims = profile
.find_related(Claims)
.all(&state.db)
.await
.context("Unable to query for related claims")?;
let Some(key) = profile
.find_related(Keys)
.one(&state.db)
.await
.context("Unable to query database for key")?
else {
bail!("The key associated with the queried profile could not be found")
};
let asp = AriadneSignatureProfile {
version: 0,
r#type: AspJwsType::Profile,
name: profile.name,
description: profile.description,
avatar_url: profile
.avatar_url
.map_or(Ok(None), |string| Url::parse(&string).map(Some))
.context("Unable to parse avatar URL from DB")?,
color: profile
.color
.map_or(Ok(None), |string| HexColor::parse_rgb(&string).map(Some))
.context("Unable to parse color from DB")?,
email: profile
.email
.map_or(Ok(None), |string| Email::from_string(string).map(Some))
.context("Unable to parse email from DB")?,
claims: claims.into_iter().map(|claim| claim.uri).collect(),
};
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];
let Ok(decrypted) = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(aes_key))
.decrypt((&aes_key[0..12]).into(), key.cipher_text.as_slice())
else {
bail!("Unable to decrypt key from database (wrong password?)")
};
let encoded = asp
.encode_and_sign(&AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?)
.context("Unable to encode the profile as a JWT and sign it")?;
println!("{encoded}");
Ok(())
}
}

View file

@ -1,6 +1,7 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
pub mod create; pub mod create;
pub mod export;
/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles. /// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles.
#[derive(Parser)] #[derive(Parser)]
@ -12,4 +13,5 @@ pub struct ProfilesSubcommand {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum ProfilesSubcommands { pub enum ProfilesSubcommands {
Create(create::ProfilesCreateCommand), Create(create::ProfilesCreateCommand),
Export(export::ProfilesExportCommand),
} }

View file

@ -7,6 +7,7 @@ use sea_orm::entity::prelude::*;
pub struct Model { pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub key: String, pub key: String,
pub alias: String,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
@ -23,7 +24,7 @@ pub enum Relation {
from = "Column::Key", from = "Column::Key",
to = "super::keys::Column::Fingerprint", to = "super::keys::Column::Fingerprint",
on_update = "Cascade", on_update = "Cascade",
on_delete = "Restrict" on_delete = "Cascade"
)] )]
Keys, Keys,
} }

View file

@ -84,6 +84,10 @@ fn main() {
let parsed = AspmCommand::parse(); let parsed = AspmCommand::parse();
let verbose = parsed.verbose; let verbose = parsed.verbose;
if verbose {
std::env::set_var("RUST_BACKTRACE", "1")
}
if let Err(e) = cli(parsed) { if let Err(e) = cli(parsed) {
match verbose { match verbose {
true => { true => {
@ -154,6 +158,7 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
}, },
AspmSubcommands::Profiles(subcommand) => match &subcommand.subcommand { AspmSubcommands::Profiles(subcommand) => match &subcommand.subcommand {
ProfilesSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime), ProfilesSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
}, },
} }
} }