mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 20:39:29 -07:00
Start on keys import subcommand, move import-gpg
This commit is contained in:
parent
ec1b9f3bf9
commit
43851d1f98
7 changed files with 143 additions and 9 deletions
|
@ -1,5 +1,5 @@
|
||||||
# aspm
|
# 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`.
|
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`.
|
|
@ -14,7 +14,7 @@ use thiserror::Error;
|
||||||
use crate::utils::jwk::JwtExt;
|
use crate::utils::jwk::JwtExt;
|
||||||
|
|
||||||
/// An enum representing the possible types of JWK for ASPs
|
/// An enum representing the possible types of JWK for ASPs
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AspKeyType {
|
pub enum AspKeyType {
|
||||||
Ed25519,
|
Ed25519,
|
||||||
ES256,
|
ES256,
|
||||||
|
|
113
src/commands/keys/import/jwk.rs
Normal file
113
src/commands/keys/import/jwk.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<AspKeyError>()
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
}
|
18
src/commands/keys/import/mod.rs
Normal file
18
src/commands/keys/import/mod.rs
Normal file
|
@ -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),
|
||||||
|
}
|
|
@ -3,8 +3,7 @@ use clap::{Parser, Subcommand};
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod generate;
|
pub mod generate;
|
||||||
#[cfg(feature = "gpg-compat")]
|
pub mod import;
|
||||||
pub mod import_gpg;
|
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
|
||||||
/// 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.
|
||||||
|
@ -19,7 +18,6 @@ pub enum KeysSubcommands {
|
||||||
Generate(generate::KeysGenerateCommand),
|
Generate(generate::KeysGenerateCommand),
|
||||||
List(list::KeysListCommand),
|
List(list::KeysListCommand),
|
||||||
Export(export::KeysExportCommand),
|
Export(export::KeysExportCommand),
|
||||||
#[cfg(feature = "gpg-compat")]
|
|
||||||
ImportGpg(import_gpg::KeysImportGpgCommand),
|
|
||||||
Delete(delete::KeysDeleteCommand),
|
Delete(delete::KeysDeleteCommand),
|
||||||
|
Import(import::KeysImportCommand),
|
||||||
}
|
}
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -6,7 +6,10 @@ use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use app_dirs2::{AppDataType, AppInfo};
|
use app_dirs2::{AppDataType, AppInfo};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use commands::{keys::KeysSubcommands, AspmSubcommand};
|
use commands::{
|
||||||
|
keys::{import::KeysImportSubcommands, KeysSubcommands},
|
||||||
|
AspmSubcommand,
|
||||||
|
};
|
||||||
use migrations::Migrator;
|
use migrations::Migrator;
|
||||||
use sea_orm::{Database, DatabaseConnection};
|
use sea_orm::{Database, DatabaseConnection};
|
||||||
use sea_orm_migration::{MigratorTrait, SchemaManager};
|
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::List(subcommand) => subcommand.execute_sync(state, runtime),
|
||||||
KeysSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
|
KeysSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
|
||||||
KeysSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime),
|
KeysSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime),
|
||||||
#[cfg(feature = "gpg-compat")]
|
KeysSubcommands::Import(subcommand) => match &subcommand.subcommand {
|
||||||
KeysSubcommands::ImportGpg(subcommand) => subcommand.execute_sync(state, runtime),
|
KeysImportSubcommands::Gpg(subcommand) => subcommand.execute_sync(state, runtime),
|
||||||
|
KeysImportSubcommands::Jwk(subcommand) => subcommand.execute_sync(state, runtime),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue