diff --git a/README.md b/README.md index dcd1187..671625b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # aspm -This is the **A**riadne **S**ignature **P**rofile **M**anager, a command line program and rust library implementing the [Ariadne Signature Profile specification v0](https://ariadne.id/related/ariadne-signature-profile-0/). Currently, it is updated to the latest version of the spec as of [ariadne/ariadne-identity-specification@84da4128b9](https://codeberg.org/ariadne/ariadne-identity-specification/commit/84da4128b90bd452544aaf422131efac5e8c312e). +This is the **A**riadne **S**ignature **P**rofile **M**anager, a command line program and rust library implementing the [Ariadne Signature Profile specification v0](https://ariadne.id/related/ariadne-signature-profile-0/). Currently, it is updated to the latest version of the spec as of [ariadne/ariadne-identity-specification@92f280bf83](https://codeberg.org/ariadne/ariadne-identity-specification/commit/92f280bf83e2d5957e5a53a6f1b6974bc975517d). The command line program is located in `src/`, and the library it uses to do ASP-related things (like creating and signing profiles, or generating keys) is located in `crates/asp`. \ No newline at end of file diff --git a/crates/asp/src/keys/mod.rs b/crates/asp/src/keys/mod.rs index 1f911eb..013a844 100644 --- a/crates/asp/src/keys/mod.rs +++ b/crates/asp/src/keys/mod.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::utils::jwk::JwtExt; /// An enum representing the possible types of JWK for ASPs -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum AspKeyType { Ed25519, ES256, diff --git a/src/commands/keys/import_gpg.rs b/src/commands/keys/import/gpg.rs similarity index 100% rename from src/commands/keys/import_gpg.rs rename to src/commands/keys/import/gpg.rs diff --git a/src/commands/keys/import/jwk.rs b/src/commands/keys/import/jwk.rs new file mode 100644 index 0000000..f1c992b --- /dev/null +++ b/src/commands/keys/import/jwk.rs @@ -0,0 +1,113 @@ +use anyhow::{anyhow, bail, Context}; +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use asp::keys::{AspKey, AspKeyError}; +use clap::{Parser, ValueEnum}; +use clap_stdin::FileOrStdin; +use data_encoding::BASE64_NOPAD; +use dialoguer::{theme::ColorfulTheme, Input, Password}; +use indoc::printdoc; +use josekit::jwk::Jwk; +use sea_orm::{ActiveValue, EntityTrait}; + +use crate::{commands::AspmSubcommand, entities::keys}; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub enum KeyGenerationType { + Ed25519, + ES256, +} + +/// Imports an ASP from raw JWK format. This only will import JWKs that have supported curves. +#[derive(Parser, Debug)] +pub struct KeysImportJwkCommand { + /// The key to import, as a file or "-" for stdin. This must be a valid JWK + key: FileOrStdin, + /// The alias of the key to import. This can be anything, and it can also be omitted to prompt interactively. This has no purpose other than providing a way to nicely name keys, rather than having to remember a fingerprint. + #[arg(short = 'n', long)] + key_alias: Option, +} + +#[async_trait::async_trait] +impl AspmSubcommand for KeysImportJwkCommand { + async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> { + let alias = if let Some(alias) = &self.key_alias { + alias.clone() + } else { + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter an alias to give to this key") + .allow_empty(false) + .interact() + .context("Unable to prompt on stderr")? + }; + + let key_password = std::env::var("KEY_PASSWORD").or_else(|_| { + Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Please enter a password to store and encrypt the key with") + .with_confirmation( + "Please confirm the password", + "The two inputs did not match!", + ) + .interact() + .context("Unable to prompt on stderr") + })?; + + let key = match AspKey::from_jwk( + Jwk::from_bytes(self.key.as_bytes()).context("Unable to parse provided JWK")?, + ) + .context("Unable to convert parsed JWK to an AspKey") + { + Ok(key) => key, + Err(e) => match e + .downcast_ref::() + .context("Invalid error returned from asp parsing")? + { + AspKeyError::InvalidJwkType => { + eprintln!("The provided JWK used a type and curve that is unsupported by ASPs, please use a correct key type"); + return Ok(()); + } + _ => return Err(e), + }, + }; + + let argon_salt = + SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes())) + .context("Unable to derive 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 encrypted = key + .export_encrypted(aes_key) + .context("Unable to derive the encryption key")?; + + // Write to db + let entry = keys::ActiveModel { + fingerprint: ActiveValue::Set(key.fingerprint.clone()), + key_type: ActiveValue::Set(key.key_type.clone().into()), + alias: ActiveValue::Set(alias), + encrypted: ActiveValue::Set(encrypted), + }; + let res = keys::Entity::insert(entry) + .exec(&state.db) + .await + .context("Unable to add key to database")?; + if res.last_insert_id != key.fingerprint { + bail!("The key was unable to be saved to the database") + } + + printdoc! { + " + Successfully imported key! + Fingerprint: {fpr} + Type: {type:?} + ", + fpr = key.fingerprint, + r#type = key.key_type + }; + + Ok(()) + } +} diff --git a/src/commands/keys/import/mod.rs b/src/commands/keys/import/mod.rs new file mode 100644 index 0000000..6fabf27 --- /dev/null +++ b/src/commands/keys/import/mod.rs @@ -0,0 +1,18 @@ +use clap::{Parser, Subcommand}; + +#[cfg(feature = "gpg-compat")] +mod gpg; +mod jwk; + +/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles. +#[derive(Parser)] +pub struct KeysImportCommand { + #[command(subcommand)] + pub subcommand: KeysImportSubcommands, +} + +#[derive(Subcommand)] +pub enum KeysImportSubcommands { + Gpg(gpg::KeysImportGpgCommand), + Jwk(jwk::KeysImportJwkCommand), +} diff --git a/src/commands/keys/mod.rs b/src/commands/keys/mod.rs index 2753e6e..70de63d 100644 --- a/src/commands/keys/mod.rs +++ b/src/commands/keys/mod.rs @@ -3,8 +3,7 @@ use clap::{Parser, Subcommand}; pub mod delete; pub mod export; pub mod generate; -#[cfg(feature = "gpg-compat")] -pub mod import_gpg; +pub mod import; pub mod list; /// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles. @@ -19,7 +18,6 @@ pub enum KeysSubcommands { Generate(generate::KeysGenerateCommand), List(list::KeysListCommand), Export(export::KeysExportCommand), - #[cfg(feature = "gpg-compat")] - ImportGpg(import_gpg::KeysImportGpgCommand), Delete(delete::KeysDeleteCommand), + Import(import::KeysImportCommand), } diff --git a/src/main.rs b/src/main.rs index c807614..6020db2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,10 @@ use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle}; use anyhow::Context; use app_dirs2::{AppDataType, AppInfo}; use clap::{Parser, Subcommand}; -use commands::{keys::KeysSubcommands, AspmSubcommand}; +use commands::{ + keys::{import::KeysImportSubcommands, KeysSubcommands}, + AspmSubcommand, +}; use migrations::Migrator; use sea_orm::{Database, DatabaseConnection}; use sea_orm_migration::{MigratorTrait, SchemaManager}; @@ -150,8 +153,10 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> { KeysSubcommands::List(subcommand) => subcommand.execute_sync(state, runtime), KeysSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime), KeysSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime), - #[cfg(feature = "gpg-compat")] - KeysSubcommands::ImportGpg(subcommand) => subcommand.execute_sync(state, runtime), + KeysSubcommands::Import(subcommand) => match &subcommand.subcommand { + KeysImportSubcommands::Gpg(subcommand) => subcommand.execute_sync(state, runtime), + KeysImportSubcommands::Jwk(subcommand) => subcommand.execute_sync(state, runtime), + }, }, } }