mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 20:39:29 -07:00
More work on profiles create, and profiles export was added
This commit is contained in:
parent
ac7785a43e
commit
86ffe01c4d
7 changed files with 182 additions and 6 deletions
|
@ -24,6 +24,15 @@ pub enum AspKeyType {
|
|||
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 {
|
||||
fn from(value: AspKeyType) -> Self {
|
||||
match value {
|
||||
|
|
|
@ -4,7 +4,7 @@ pub struct Migration;
|
|||
|
||||
impl MigrationName for Migration {
|
||||
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_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.col(ColumnDef::new(Profiles::Alias).string().not_null())
|
||||
.col(ColumnDef::new(Profiles::Name).string().not_null())
|
||||
.col(ColumnDef::new(Profiles::Description).string().null())
|
||||
.col(ColumnDef::new(Profiles::AvatarUrl).string().null())
|
||||
|
@ -88,6 +89,7 @@ impl MigrationTrait for Migration {
|
|||
pub enum Profiles {
|
||||
Table,
|
||||
Key,
|
||||
Alias,
|
||||
Name,
|
||||
Description,
|
||||
AvatarUrl,
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use asp::{profiles::*, utils::jwt::AspJwsType};
|
||||
use asp::{keys::AspKeyType, profiles::*, utils::jwt::AspJwsType};
|
||||
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.
|
||||
/// 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")?
|
||||
};
|
||||
|
||||
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 {
|
||||
version: 0,
|
||||
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))?,
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
101
src/commands/profiles/export.rs
Normal file
101
src/commands/profiles/export.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
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.
|
||||
#[derive(Parser)]
|
||||
|
@ -12,4 +13,5 @@ pub struct ProfilesSubcommand {
|
|||
#[derive(Subcommand)]
|
||||
pub enum ProfilesSubcommands {
|
||||
Create(create::ProfilesCreateCommand),
|
||||
Export(export::ProfilesExportCommand),
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use sea_orm::entity::prelude::*;
|
|||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub key: String,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
|
@ -23,7 +24,7 @@ pub enum Relation {
|
|||
from = "Column::Key",
|
||||
to = "super::keys::Column::Fingerprint",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Restrict"
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Keys,
|
||||
}
|
||||
|
|
|
@ -84,6 +84,10 @@ fn main() {
|
|||
let parsed = AspmCommand::parse();
|
||||
let verbose = parsed.verbose;
|
||||
|
||||
if verbose {
|
||||
std::env::set_var("RUST_BACKTRACE", "1")
|
||||
}
|
||||
|
||||
if let Err(e) = cli(parsed) {
|
||||
match verbose {
|
||||
true => {
|
||||
|
@ -154,6 +158,7 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
|
|||
},
|
||||
AspmSubcommands::Profiles(subcommand) => match &subcommand.subcommand {
|
||||
ProfilesSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
ProfilesSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue