diff --git a/crates/asp/src/aspe/mod.rs b/crates/asp/src/aspe/mod.rs index c7e44f5..e3273e6 100644 --- a/crates/asp/src/aspe/mod.rs +++ b/crates/asp/src/aspe/mod.rs @@ -166,4 +166,10 @@ mod tests { let result = AspeServer::new(Host::Domain(String::from("keyoxide.org"))).await; assert!(result.is_ok(), "Constructing an AspeServer should succeed") } + + #[tokio::test] + async fn building_invalid_aspe_server_fails() { + let result = AspeServer::new(Host::Domain(String::from("example.com"))).await; + assert!(result.is_err(), "Constructing an AspeServer with an invalid domain should fail") + } } diff --git a/crates/aspm/src/commands/aspe/create.rs b/crates/aspm/src/commands/aspe/create.rs index d147d4e..32ccbc0 100644 --- a/crates/aspm/src/commands/aspe/create.rs +++ b/crates/aspm/src/commands/aspe/create.rs @@ -3,7 +3,8 @@ use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _}; use asp::{ aspe::{ - requests::{AspeRequest, AspeRequestType, AspeRequestVariant}, AspeRequestFailure, AspeServer + requests::{AspeRequest, AspeRequestType, AspeRequestVariant}, + AspeRequestFailure, AspeServer, }, hex_color::HexColor, keys::AspKey, @@ -130,14 +131,20 @@ impl AspmSubcommand for AspeCreateCommand { .context("Unable to encode the profile as a JWT and sign it")?; match server.post_request(encoded_request).await { - Ok(_) => { + Ok(_) => { println!("Successfully uploaded profile!"); Ok(()) - }, - Err(AspeRequestFailure::BadRequest) => bail!("The ASPE server rejected the request due to invalid data"), - Err(AspeRequestFailure::TooLarge) => bail!("The ASPE server rejected the request as being too large"), - Err(AspeRequestFailure::RateLimited) => bail!("The ASPE server rejected the request due to a ratelimit"), - Err(AspeRequestFailure::Unknown(e)) => Err(e.into()) - } + } + Err(AspeRequestFailure::BadRequest) => { + bail!("The ASPE server rejected the request due to invalid data") + } + Err(AspeRequestFailure::TooLarge) => { + bail!("The ASPE server rejected the request as being too large") + } + Err(AspeRequestFailure::RateLimited) => { + bail!("The ASPE server rejected the request due to a ratelimit") + } + Err(AspeRequestFailure::Unknown(e)) => Err(e.into()), + } } } diff --git a/crates/aspm/src/commands/aspe/mod.rs b/crates/aspm/src/commands/aspe/mod.rs index d06d606..f29ba09 100644 --- a/crates/aspm/src/commands/aspe/mod.rs +++ b/crates/aspm/src/commands/aspe/mod.rs @@ -11,5 +11,5 @@ pub struct AspeSubcommand { #[derive(Subcommand)] pub enum AspeSubcommands { - Create(create::AspeCreateCommand), + Create(create::AspeCreateCommand), } diff --git a/crates/aspm/src/commands/keys/delete.rs b/crates/aspm/src/commands/keys/delete.rs index e730ae4..8a5d181 100644 --- a/crates/aspm/src/commands/keys/delete.rs +++ b/crates/aspm/src/commands/keys/delete.rs @@ -8,10 +8,7 @@ use sea_orm::ModelTrait as _; use std::io::Write; -use crate::{ - commands::AspmSubcommand, - utils, -}; +use crate::{commands::AspmSubcommand, utils}; /// Deletes a saved key, after asking for confirmation. #[derive(Parser, Debug)] diff --git a/crates/aspm/src/commands/keys/export.rs b/crates/aspm/src/commands/keys/export.rs index 6894e3a..5c05f45 100644 --- a/crates/aspm/src/commands/keys/export.rs +++ b/crates/aspm/src/commands/keys/export.rs @@ -45,7 +45,7 @@ pub enum KeyExportFormat { Jwk, } -/// Exports a saved key, specified by its fingerprint, into multiple formats. Run this command with `--help` in order to see a list of the possible formats, and explanations for all of them. +/// Exports a saved key, specified by its fingerprint, into multiple formats. Run this command with `--help` in order to see a list of the possible formats, and explanations for all of them. This command will by default interactively prompt for the key's password in order to be decrypted, unless the KEY_PASSWORD environment variable is present. #[derive(Parser, Debug)] pub struct KeysExportCommand { /// The format to export the key into. All of these formats can then be correctly imported back into this tool, but only some are encrypted, so keep that in mind when handling the exported keys. diff --git a/crates/aspm/src/commands/mod.rs b/crates/aspm/src/commands/mod.rs index 2f9f128..7d09cc3 100644 --- a/crates/aspm/src/commands/mod.rs +++ b/crates/aspm/src/commands/mod.rs @@ -1,6 +1,6 @@ +pub mod aspe; pub mod keys; pub mod profiles; -pub mod aspe; use clap::Parser; use tokio::runtime::Runtime; diff --git a/crates/aspm/src/commands/profiles/import.rs b/crates/aspm/src/commands/profiles/import.rs index 8090d0d..24cb2c5 100644 --- a/crates/aspm/src/commands/profiles/import.rs +++ b/crates/aspm/src/commands/profiles/import.rs @@ -32,7 +32,8 @@ impl AspmSubcommand for ProfilesImportCommand { (host.to_string(), fingerprint.to_string()) }) }) { - Some((host, fingerprint)) => match aspe::AspeServer::new(Host::parse(&host).unwrap()).await? + Some((host, fingerprint)) => match aspe::AspeServer::new(Host::parse(&host).unwrap()) + .await? .fetch_profile(&fingerprint) .await { diff --git a/crates/aspm/src/main.rs b/crates/aspm/src/main.rs index 77e2a3d..6598ed5 100644 --- a/crates/aspm/src/main.rs +++ b/crates/aspm/src/main.rs @@ -7,7 +7,9 @@ use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle}; use anyhow::Context; use app_dirs2::{AppDataType, AppInfo}; use clap::{Parser, Subcommand}; -use commands::{aspe::AspeSubcommands, keys::KeysSubcommands, profiles::ProfilesSubcommands, AspmSubcommand}; +use commands::{ + aspe::AspeSubcommands, keys::KeysSubcommands, profiles::ProfilesSubcommands, AspmSubcommand, +}; use migrations::{Migrator, MigratorTrait, SchemaManager}; use sea_orm::{Database, DatabaseConnection}; use thiserror::Error; @@ -173,7 +175,7 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> { }, AspmSubcommands::Aspe(subcommand) => match subcommand.subcommand { AspeSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime), - } + }, } } diff --git a/crates/aspm/src/utils.rs b/crates/aspm/src/utils.rs index c0fc618..fab1cfa 100644 --- a/crates/aspm/src/utils.rs +++ b/crates/aspm/src/utils.rs @@ -17,9 +17,10 @@ pub mod keys { fingerprint: S, password: P, ) -> anyhow::Result> { - let argon_salt = - SaltString::from_b64(&BASE64_NOPAD.encode(fingerprint.as_ref().to_uppercase().as_bytes())) - .context("Unable to derive argon2 salt")?; + let argon_salt = SaltString::from_b64( + &BASE64_NOPAD.encode(fingerprint.as_ref().to_uppercase().as_bytes()), + ) + .context("Unable to derive argon2 salt")?; let argon2 = Argon2::default(); let hash = argon2 .hash_password(password.as_ref(), &argon_salt) diff --git a/crates/aspm/tests/cli.rs b/crates/aspm/tests/cli.rs index f29c15c..21237b1 100644 --- a/crates/aspm/tests/cli.rs +++ b/crates/aspm/tests/cli.rs @@ -4,8 +4,28 @@ use predicates::prelude::*; use tempfile::TempDir; use std::process::Command; +static KEY_ALIAS: &str = "TESTKEY"; +static KEY_PASSWORD: &str = "TESTKEYPASSWORD"; + +// TODO: Also test ed25519 key generation +fn assert_key_generated(datadir: &str) -> Result<(), anyhow::Error> { + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("keys") + .arg("generate") + .env("KEY_PASSWORD", KEY_PASSWORD) + .arg("es256") + .arg("--key-alias") + .arg(KEY_ALIAS) + .assert() + .success() + .stdout(predicate::str::starts_with("Successfully generated a new key!")); + + Ok(()) +} + #[test] -fn help_prints_correctly() -> Result<(), anyhow::Error> { +fn help_works() -> Result<(), anyhow::Error> { let tempdir = TempDir::new()?; let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?; @@ -23,6 +43,13 @@ fn help_prints_correctly() -> Result<(), anyhow::Error> { .success() .stdout(predicate::str::starts_with(env!("CARGO_PKG_DESCRIPTION"))); + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("help") + .assert() + .success() + .stdout(predicate::str::starts_with(env!("CARGO_PKG_DESCRIPTION"))); + Command::cargo_bin("aspm")? .env("ASPM_DATA_DIR", datadir) .assert() @@ -31,3 +58,81 @@ fn help_prints_correctly() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +fn keys_generate_works() -> Result<(), anyhow::Error> { + let tempdir = TempDir::new()?; + let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?; + + assert_key_generated(datadir) +} + +#[test] +fn keys_list_works() -> Result<(), anyhow::Error> { + let tempdir = TempDir::new()?; + let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?; + + assert_key_generated(datadir)?; + + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("keys") + .arg("list") + .assert() + .success() + .stdout(predicate::str::contains(KEY_ALIAS)); + + Ok(()) +} + +// This test takes a bit due to testing each export format individually, causing the password to be hashed multiple times +#[test] +fn keys_export_works() -> Result<(), anyhow::Error> { + let tempdir = TempDir::new()?; + let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?; + + assert_key_generated(datadir)?; + + for export_format in ["pkcs8", "asp-tool", "jwk"] { + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("keys") + .arg("export") + .env("KEY_PASSWORD", KEY_PASSWORD) + .arg(export_format) + .arg(KEY_ALIAS) + .assert() + .success() + .stderr(predicate::str::contains("Exported key \"")); + } + + Ok(()) +} + +#[test] +fn keys_delete_works() -> Result<(), anyhow::Error> { + let tempdir = TempDir::new()?; + let datadir = tempdir.path().to_str().context("Tempdir path was not valid utf8")?; + + assert_key_generated(datadir)?; + + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("keys") + .arg("delete") + .arg("--no-confirm") + .arg(KEY_ALIAS) + .assert() + .success() + .stdout(predicate::str::contains("Successfully deleted key with fingerprint ")); + + Command::cargo_bin("aspm")? + .env("ASPM_DATA_DIR", datadir) + .arg("keys") + .arg("list") + .assert() + .success() + .stdout(predicate::str::contains("Saved keys (0 total):")); + + Ok(()) +} \ No newline at end of file