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,
|
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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
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};
|
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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue