diff --git a/crates/asp/src/aspe/requests.rs b/crates/asp/src/aspe/requests.rs index 8503025..409f9cd 100644 --- a/crates/asp/src/aspe/requests.rs +++ b/crates/asp/src/aspe/requests.rs @@ -72,7 +72,10 @@ mod tests { let key = TryInto::::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap()) .unwrap(); - let decoded = AspeRequest::decode_and_verify(crate::test_constants::CREATE_REQUEST, Some(&key.fingerprint)); + let decoded = AspeRequest::decode_and_verify( + crate::test_constants::CREATE_REQUEST, + Some(&key.fingerprint), + ); assert!( decoded.is_ok(), "ASPE request JWS should verify and decode successfully" @@ -119,7 +122,10 @@ mod tests { let key = TryInto::::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap()) .unwrap(); - let decoded = AspeRequest::decode_and_verify(crate::test_constants::UPDATE_REQUEST, Some(&key.fingerprint)); + let decoded = AspeRequest::decode_and_verify( + crate::test_constants::UPDATE_REQUEST, + Some(&key.fingerprint), + ); assert!( decoded.is_ok(), "ASPE request JWS should verify and decode successfully" @@ -166,7 +172,10 @@ mod tests { let key = TryInto::::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap()) .unwrap(); - let decoded = AspeRequest::decode_and_verify(crate::test_constants::DELETE_REQUEST, Some(&key.fingerprint)); + let decoded = AspeRequest::decode_and_verify( + crate::test_constants::DELETE_REQUEST, + Some(&key.fingerprint), + ); assert!( decoded.is_ok(), "ASPE request JWS should verify and decode successfully" diff --git a/crates/asp/src/profiles/mod.rs b/crates/asp/src/profiles/mod.rs index 95c4059..3919591 100644 --- a/crates/asp/src/profiles/mod.rs +++ b/crates/asp/src/profiles/mod.rs @@ -45,8 +45,8 @@ mod tests { use hex_color::HexColor; use josekit::jwk::Jwk; -use serde_email::Email; -use url::Url; + use serde_email::Email; + use url::Url; use crate::{ keys::AspKey, @@ -83,8 +83,10 @@ use url::Url; let key = TryInto::::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap()) .unwrap(); - let profile = - AriadneSignatureProfile::decode_and_verify(crate::test_constants::PROFILE, Some(&key.fingerprint)); + let profile = AriadneSignatureProfile::decode_and_verify( + crate::test_constants::PROFILE, + Some(&key.fingerprint), + ); assert!(profile.is_ok(), "Profile should parse and verify correctly"); let (_, profile) = profile.unwrap(); assert_eq!( diff --git a/crates/asp/src/utils/jwt.rs b/crates/asp/src/utils/jwt.rs index a93de58..7f25fe4 100644 --- a/crates/asp/src/utils/jwt.rs +++ b/crates/asp/src/utils/jwt.rs @@ -20,7 +20,10 @@ pub trait JwtSerializable {} pub trait JwtSerialize { fn encode_and_sign(&self, key: &AspKey) -> Result; - fn decode_and_verify(jwt: &str, expected_fingerprint: Option) -> Result<(AspKey, Box), JwtDeserializationError> + fn decode_and_verify( + jwt: &str, + expected_fingerprint: Option, + ) -> Result<(AspKey, Box), JwtDeserializationError> where Self: for<'de> Deserialize<'de> + JwtSerializable, S: AsRef; @@ -80,10 +83,13 @@ impl Deserialize<'de>> JwtSerialize fo .or(Err(JwtSerializationError::SerializationError)) } - fn decode_and_verify(jwt: &str, expected_fingerprint: Option) -> Result<(AspKey, Box), JwtDeserializationError> + fn decode_and_verify( + jwt: &str, + expected_fingerprint: Option, + ) -> Result<(AspKey, Box), JwtDeserializationError> where Self: for<'de> serde::Deserialize<'de> + JwtSerializable, - S: AsRef + S: AsRef, { // Decode the header and check if the key id is correct let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?; diff --git a/src/commands/keys/export.rs b/src/commands/keys/export.rs index 0367dff..c465ced 100644 --- a/src/commands/keys/export.rs +++ b/src/commands/keys/export.rs @@ -10,32 +10,32 @@ use clap::{Parser, ValueEnum}; use data_encoding::{BASE64, BASE64_NOPAD}; use dialoguer::{theme::ColorfulTheme, Password}; use josekit::jwk::Jwk; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::{ commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult}, entities::prelude::*, }; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(tag = "alg", rename = "scrypt")] pub struct ASPToolScryptExport { #[serde(rename = "prm")] - parameters: ASPToolScryptExportParam, + pub parameters: ASPToolScryptExportParam, #[serde(rename = "slt")] - salt: String, + pub salt: String, #[serde(rename = "key")] - encrypted_key: String, + pub encrypted_key: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ASPToolScryptExportParam { #[serde(rename = "N")] - cost: u64, + pub cost: u64, #[serde(rename = "r")] - block_size: u32, + pub block_size: u32, #[serde(rename = "p")] - parallelism: u32, + pub parallelism: u32, } #[derive(ValueEnum, Debug, Clone)] diff --git a/src/commands/keys/import.rs b/src/commands/keys/import.rs index 40ae311..46dc70a 100644 --- a/src/commands/keys/import.rs +++ b/src/commands/keys/import.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use asp::keys::AspKey; use clap::{Parser, ValueEnum}; -use data_encoding::{BASE64URL_NOPAD, BASE64_NOPAD}; +use data_encoding::{BASE64, BASE64URL_NOPAD, BASE64_NOPAD}; use dialoguer::{theme::ColorfulTheme, Input, Password}; use aes_gcm::{ @@ -13,7 +13,10 @@ use indoc::printdoc; use josekit::jwk::Jwk; use sea_orm::{ActiveValue, EntityTrait}; -use crate::{commands::AspmSubcommand, entities::keys}; +use crate::{ + commands::{keys::export::ASPToolScryptExport, AspmSubcommand}, + entities::keys, +}; #[derive(ValueEnum, Debug, Clone)] pub enum KeyImportFormat { @@ -22,13 +25,15 @@ pub enum KeyImportFormat { // Imports a PGP key from a local GPG store #[cfg(feature = "gpg-compat")] Gpg, + // Imports a secret key in the format used by https://asp.keyoxide.org + AspTool, } /// Imports an ASP from raw JWK format. This only will import JWKs that have supported curves. #[derive(Parser, Debug)] pub struct KeysImportCommand { /// The format of key to import. - /// + /// format: KeyImportFormat, /// The key to import, as a string. key: String, @@ -236,6 +241,53 @@ impl AspmSubcommand for KeysImportCommand { .context("Unable to construct Jwk representation of PGP key")?, )? } + KeyImportFormat::AspTool => { + let parsed: ASPToolScryptExport = serde_json::from_slice( + BASE64 + .decode(self.key.as_bytes()) + .context("Unable to base64 decode asp-tool secret key")? + .as_slice(), + ) + .context("Unable to parse base64-decoded asp-tool secret key as JSON")?; + + let mut derived_key = vec![0u8; parsed.encrypted_key.len()]; + scrypt::scrypt( + key_password.as_bytes(), + &BASE64 + .decode(parsed.salt.as_bytes()) + .context("Unable to base64 decode salt from parsed asp-tool secret key")?, + &scrypt::Params::new( + parsed + .parameters + .cost + .ilog2() + .try_into() + .context("log_2(N) from parsed asp-tool secret key was not a u8")?, + parsed.parameters.block_size, + parsed.parameters.parallelism, + // This length is unused in the scrypt function itself, so the recommended works as a placeholder + scrypt::Params::RECOMMENDED_LEN, + ) + .context( + "Unable to assemble scrypt parameters from parsed asp-tool secret key", + )?, + &mut derived_key, + ) + .context("Unable to derive PKCS#8 encryption key with scrypt")?; + + AspKey::from_pkcs8( + &BASE64_NOPAD.encode( + BASE64.decode(parsed.encrypted_key.as_bytes()) + .context("Unable to base64 decode encrypted key from parsed asp-tool secret key")? + .into_iter() + .enumerate() + .map(|(i, byte)| byte ^ derived_key[i]) + .collect::>() + .as_slice(), + ), + ) + .context("Unable to decode decrypted asp-tool key as PKCS#8")? + } }; let argon_salt = SaltString::from_b64( diff --git a/src/main.rs b/src/main.rs index a13a8b0..3867df5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,7 +143,11 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> { .context("Unable to check database for keys table")?); // Make the state - let state = AspmState { data_dir, db, verbose: parsed.verbose }; + let state = AspmState { + data_dir, + db, + verbose: parsed.verbose, + }; Ok::(state) })?;