diff --git a/Cargo.lock b/Cargo.lock index 7a8d0e3..8bdf64e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.3" @@ -28,6 +38,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.7" @@ -195,6 +219,7 @@ dependencies = [ name = "aspm" version = "0.1.0" dependencies = [ + "aes-gcm", "anstyle", "anyhow", "app_dirs2", @@ -748,6 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -761,6 +787,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.1" @@ -1258,6 +1293,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1934,6 +1979,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.63" @@ -2187,6 +2238,18 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3587,6 +3650,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index cac1dff..a159fcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ gpgme = { version = "0.11.0", optional = true } pgp = { version = "0.10.2", optional = true } josekit = { version = "0.8.5", optional = true } elliptic-curve = { version = "0.13.8", optional = true } +aes-gcm = "0.10.3" [features] gpg-compat = ["dep:gpgme", "dep:pgp", "dep:josekit", "dep:elliptic-curve"] diff --git a/crates/asp/src/keys/mod.rs b/crates/asp/src/keys/mod.rs index 013a844..19e5209 100644 --- a/crates/asp/src/keys/mod.rs +++ b/crates/asp/src/keys/mod.rs @@ -1,17 +1,21 @@ -use anyhow::bail; +use anyhow::{bail, Context}; +use data_encoding::{BASE32_NOPAD, BASE64_NOPAD}; use josekit::{ jwk::{ - alg::{ec::EcCurve, ed::EdCurve}, - Jwk, + alg::{ + ec::{EcCurve, EcKeyPair}, + ed::{EdCurve, EdKeyPair}, + }, + Jwk, KeyPair, }, jws::{ alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa}, - JwsHeader, JwsSigner, JwsVerifier, + JwsHeader, JwsSigner, JwsVerifier, ES256, }, }; -use thiserror::Error; - -use crate::utils::jwk::JwtExt; +use openssl::pkey::PKey; +use serde_json::Map; +use sha2::{Digest, Sha512}; /// An enum representing the possible types of JWK for ASPs #[derive(Debug, Clone)] @@ -49,64 +53,119 @@ pub struct AspKey { } impl AspKey { - pub fn from_jwk(jwk: Jwk) -> Result { + /// Calculates the ASP fingerprint of an arbitrary [Jwk] + pub fn calculate_fingerprint(jwk: &Jwk) -> Result { + // Construct a JSON object with only the "crv", "kty", "x", and potentially "y" values + let fingerprint: String = { + let mut map = Map::new(); + map.insert( + "crv".to_string(), + jwk.curve() + .context("Key did not contain a 'crv' value")? + .into(), + ); + map.insert("kty".to_string(), jwk.key_type().into()); + map.insert( + "x".to_string(), + jwk.parameter("x") + .context("Key did not contain an 'x' value")? + .clone(), + ); + if let Some(y) = jwk.parameter("y") { + map.insert("y".to_string(), y.clone()); + } + + serde_json::to_string(&map) + .context("Unable to serialize key into ordered, minimal JSON")? + }; + // Sha512 hash the JSON + let fingerprint: Vec = { + let mut hash = Sha512::new(); + hash.update(fingerprint); + hash.finalize().to_vec() + }; + // Get the first 16 bytes of the hash + let fingerprint = &fingerprint[0..16]; + // Base32 encode the first 16 bytes, and that is the fingerprint + let fingerprint = BASE32_NOPAD.encode(fingerprint); + Ok(fingerprint) + } + + /// Creates an [AspKey] from an arbitrary [Jwk] of the correct key type + pub fn from_jwk(jwk: Jwk) -> Result { + // Calculate the fingerprint match jwk.key_type() { "OKP" => match jwk.curve() { Some("Ed25519") => Ok(Self { key_type: AspKeyType::Ed25519, - fingerprint: jwk - .get_fingerprint() - .or(Err(AspKeyError::FingerprintError))?, + fingerprint: Self::calculate_fingerprint(&jwk)?, jwk, }), - _ => Err(AspKeyError::InvalidJwkType), + _ => bail!("Invalid JWK type"), }, "EC" => match jwk.curve() { Some("P-256") => Ok(Self { key_type: AspKeyType::ES256, - fingerprint: jwk - .get_fingerprint() - .or(Err(AspKeyError::FingerprintError))?, + fingerprint: Self::calculate_fingerprint(&jwk)?, jwk, }), - _ => Err(AspKeyError::InvalidJwkType), + _ => bail!("Invalid JWK type"), }, - _ => Err(AspKeyError::InvalidJwkType), + _ => bail!("Invalid JWK type"), } } - pub fn from_pkcs8(key: &str) -> Result { - Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?) + pub fn from_pkcs8(key: &str) -> Result { + let decoded_pkcs8 = BASE64_NOPAD.decode(key.as_bytes())?; + let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?; + + Self::from_jwk(key_pair.to_jwk_key_pair()) } - pub fn into_pkcs8(&self) -> Result { - self.jwk - .to_pkcs8() - .or(Err(AspKeyError::Pkcs8ConversionError)) + pub fn into_pkcs8(&self) -> Result { + // This was done because I couldn't find an easy way to get a PKCS#8 encoded private key from the josekit Jwk struct, so I did the following as a workaround, directly using the openssl library: + // 1. Get the josekit EcKeyPair from the Jwk + // 2. Convert that to a PEM private key + // 3. Get the openssl Pkey struct by loading the PRM private key + // 4. Convert that Pkey into the PKCS#8 encoded private key (and then base64 encode it) + let key_pair: Box = match self.jwk.key_type() { + "EC" => match self.jwk.curve() { + Some("P-256") => Box::new(EcKeyPair::from_jwk(&self.jwk)?), + _ => bail!("Unsupported curve type"), + }, + "OKP" => match self.jwk.curve() { + Some("Ed25519") => Box::new(EdKeyPair::from_jwk(&self.jwk)?), + _ => bail!("Unsupported curve type"), + }, + _ => bail!("Unsupported key type"), + }; + let pem_private = key_pair.to_pem_private_key(); + let pkey = PKey::private_key_from_pem(&pem_private)?; + let pkcs8 = pkey.as_ref().private_key_to_pkcs8()?; + let encoded = BASE64_NOPAD.encode(&pkcs8); + + Ok(encoded) } - pub fn generate(key_type: AspKeyType) -> Result { - (|| -> anyhow::Result { - match key_type { - AspKeyType::Ed25519 => { - let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?; - Ok(Self { - key_type, - fingerprint: jwk.get_fingerprint()?, - jwk, - }) - } - AspKeyType::ES256 => { - let jwk = Jwk::generate_ec_key(EcCurve::P256)?; - Ok(Self { - key_type, - fingerprint: jwk.get_fingerprint()?, - jwk, - }) - } + pub fn generate(key_type: AspKeyType) -> Result { + match key_type { + AspKeyType::Ed25519 => { + let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?; + Ok(Self { + key_type, + fingerprint: Self::calculate_fingerprint(&jwk)?, + jwk, + }) } - })() - .or(Err(AspKeyError::GenerationError)) + AspKeyType::ES256 => { + let jwk = Jwk::generate_ec_key(EcCurve::P256)?; + Ok(Self { + key_type, + fingerprint: Self::calculate_fingerprint(&jwk)?, + jwk, + }) + } + } } pub fn create_signer(&self) -> anyhow::Result> { @@ -122,19 +181,10 @@ impl AspKey { AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?), }) } - - pub fn export_encrypted(&self, secret: &[u8]) -> anyhow::Result { - self.jwk.encrypt(secret) - } - - pub fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result { - let jwk = Jwk::from_encrypted(secret, jwe)?; - Ok(Self::from_jwk(jwk)?) - } } impl TryFrom for AspKey { - type Error = AspKeyError; + type Error = anyhow::Error; fn try_from(value: Jwk) -> Result { Self::from_jwk(value) @@ -157,18 +207,6 @@ impl JwsHeaderExt for JwsHeader { } } -#[derive(Error, Debug, PartialEq)] -pub enum AspKeyError { - #[error("provided jwk was not a valid type")] - InvalidJwkType, - #[error("unable to calculate fingerprint of key")] - FingerprintError, - #[error("an error occurred during key generation")] - GenerationError, - #[error("unable to convert PKCS#8 key to/from a jwt key")] - Pkcs8ConversionError, -} - #[cfg(test)] mod tests { use josekit::jwk::{ @@ -176,7 +214,7 @@ mod tests { Jwk, }; - use crate::keys::{AspKey, AspKeyError, AspKeyType}; + use crate::keys::{AspKey, AspKeyType}; #[test] fn generate_eddsa() { @@ -239,31 +277,6 @@ mod tests { let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type! assert!(jwk.is_ok(), "jwk should generate successfully"); let key = AspKey::from_jwk(jwk.unwrap()); - assert_eq!( - key.err(), - Some(AspKeyError::InvalidJwkType), - "key should fail to convert" - ); - } - - #[test] - fn export_encrypted() { - let mut secret = [0u8; 32]; - assert!(openssl::rand::rand_bytes(&mut secret).is_ok()); - let key = AspKey::generate(AspKeyType::Ed25519); - assert!(key.is_ok()); - let jwe = key.unwrap().export_encrypted(&secret); - assert!(jwe.is_ok()); - } - - #[test] - fn import_encrypted() { - let mut secret = [0u8; 32]; - assert!(openssl::rand::rand_bytes(&mut secret).is_ok()); - let key = AspKey::generate(AspKeyType::Ed25519).unwrap(); - let encrypted = key.export_encrypted(&secret); - assert!(encrypted.is_ok()); - let decrypted = AspKey::from_encrypted(&secret, &encrypted.unwrap()); - assert!(decrypted.is_ok()); + assert!(key.is_err(), "key should fail to convert"); } } diff --git a/crates/asp/src/utils/jwk.rs b/crates/asp/src/utils/jwk.rs deleted file mode 100644 index d1d4f50..0000000 --- a/crates/asp/src/utils/jwk.rs +++ /dev/null @@ -1,112 +0,0 @@ -use anyhow::{bail, Context}; -use data_encoding::{BASE32_NOPAD, BASE64_NOPAD}; -use josekit::{ - jwe::JweHeader, - jwe::{self, alg::aesgcmkw::AesgcmkwJweAlgorithm::A256gcmkw}, - jwk::{ - alg::{ec::EcKeyPair, ed::EdKeyPair}, - Jwk, KeyPair, - }, - jws::ES256, -}; -use openssl::pkey::PKey; -use serde_json::Map; -use sha2::{Digest, Sha512}; - -pub trait JwtExt { - fn get_fingerprint(&self) -> anyhow::Result; - fn to_pkcs8(&self) -> anyhow::Result; - fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result; - fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result; - fn encrypt(&self, secret: &[u8]) -> anyhow::Result; -} - -impl JwtExt for Jwk { - fn get_fingerprint(&self) -> anyhow::Result { - // Construct a JSON object with only the "crv", "kty", "x", and potentially "y" values - let fingerprint: String = { - let mut map = Map::new(); - map.insert( - "crv".to_string(), - self.curve() - .context("Key did not contain a 'crv' value")? - .into(), - ); - map.insert("kty".to_string(), self.key_type().into()); - map.insert( - "x".to_string(), - self.parameter("x") - .context("Key did not contain an 'x' value")? - .clone(), - ); - if let Some(y) = self.parameter("y") { - map.insert("y".to_string(), y.clone()); - } - - serde_json::to_string(&map) - .context("Unable to serialize key into ordered, minimal JSON")? - }; - // Sha512 hash the JSON - let fingerprint: Vec = { - let mut hash = Sha512::new(); - hash.update(fingerprint); - hash.finalize().to_vec() - }; - // Get the first 16 bytes of the hash - let fingerprint = &fingerprint[0..16]; - // Base32 encode the first 16 bytes, and that is the fingerprint - let fingerprint = BASE32_NOPAD.encode(fingerprint); - Ok(fingerprint) - } - - fn to_pkcs8(&self) -> anyhow::Result { - // This was done because I couldn't find an easy way to get a PKCS#8 encoded private key from the josekit Jwk struct, so I did the following as a workaround, directly using the openssl library: - // 1. Get the josekit EcKeyPair from the Jwk - // 2. Convert that to a PEM private key - // 3. Get the openssl Pkey struct by loading the PRM private key - // 4. Convert that Pkey into the PKCS#8 encoded private key (and then base64 encode it) - let key_pair: Box = match self.key_type() { - "EC" => match self.curve() { - Some("P-256") => Box::new(EcKeyPair::from_jwk(self)?), - _ => bail!("Unsupported curve type"), - }, - "OKP" => match self.curve() { - Some("Ed25519") => Box::new(EdKeyPair::from_jwk(self)?), - _ => bail!("Unsupported curve type"), - }, - _ => bail!("Unsupported key type"), - }; - let pem_private = key_pair.to_pem_private_key(); - let pkey = PKey::private_key_from_pem(&pem_private)?; - let pkcs8 = pkey.as_ref().private_key_to_pkcs8()?; - let encoded = BASE64_NOPAD.encode(&pkcs8); - Ok(encoded) - } - - fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result { - let decoded_pkcs8 = BASE64_NOPAD.decode(pkcs8)?; - let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?; - Ok(key_pair.to_jwk_key_pair()) - } - - fn encrypt(&self, secret: &[u8]) -> anyhow::Result { - let mut header = JweHeader::new(); - header.set_content_type("jwt+json"); - header.set_content_encryption("A128CBC-HS256"); - - let payload = self.to_string(); - - let encrypter = A256gcmkw.encrypter_from_bytes(secret)?; - let jwt = jwe::serialize_compact(payload.as_bytes(), &header, &encrypter)?; - - Ok(jwt) - } - - fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result { - let decrypter = A256gcmkw.decrypter_from_bytes(secret)?; - let (deserialized, _) = jwe::deserialize_compact(jwe, &decrypter)?; - let jwk = Jwk::from_bytes(deserialized)?; - - Ok(jwk) - } -} diff --git a/crates/asp/src/utils/mod.rs b/crates/asp/src/utils/mod.rs index 9332967..417233c 100644 --- a/crates/asp/src/utils/mod.rs +++ b/crates/asp/src/utils/mod.rs @@ -1,2 +1 @@ -pub mod jwk; pub mod jwt; diff --git a/src/commands/keys/delete.rs b/src/commands/keys/delete.rs index cdefb26..4c8fcff 100644 --- a/src/commands/keys/delete.rs +++ b/src/commands/keys/delete.rs @@ -10,7 +10,7 @@ use std::io::Write; use crate::{ commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult}, - entities::prelude::* + entities::prelude::*, }; /// Deletes a saved key, after asking for confirmation. diff --git a/src/commands/keys/export.rs b/src/commands/keys/export.rs index fff9e25..f10683b 100644 --- a/src/commands/keys/export.rs +++ b/src/commands/keys/export.rs @@ -1,3 +1,7 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, +}; use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle}; use anyhow::{anyhow, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; @@ -6,18 +10,17 @@ use clap::{Parser, ValueEnum}; use data_encoding::BASE64_NOPAD; use dialoguer::{theme::ColorfulTheme, Password}; use indoc::writedoc; +use josekit::jwk::Jwk; use std::io::Write; use crate::{ commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult}, - entities::prelude::* + entities::prelude::*, }; #[derive(ValueEnum, Debug, Clone)] pub enum KeyExportFormat { - /// An encrypted JWE format, the same way it is stored internally. This is likely only compatible with this tool specifically due to how it is decrypted. - Encrypted, /// An unencrypted PKCS#8 format. This is the format used by the asp.keyoxide.org web tool. #[clap(alias = "PKCS#8")] PKCS8, @@ -83,11 +86,12 @@ impl AspmSubcommand for KeysExportCommand { let aes_key = hash.hash.context("Unable to derive encryption key")?; let aes_key = &aes_key.as_bytes()[0..32]; - if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &key.encrypted) { + if let Ok(decrypted) = Aes256Gcm::new(&Key::::from_slice(aes_key)) + .decrypt((&aes_key[0..12]).into(), key.cipher_text.as_slice()) + { + let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?; + let export = match self.format { - KeyExportFormat::Encrypted => decrypted - .export_encrypted(aes_key) - .context("Unable to convert key into encrypted format")?, KeyExportFormat::PKCS8 => decrypted .into_pkcs8() .context("Unable to convert key into PKCS#8 format")?, diff --git a/src/commands/keys/generate.rs b/src/commands/keys/generate.rs index 97f3d9b..6bbb9c3 100644 --- a/src/commands/keys/generate.rs +++ b/src/commands/keys/generate.rs @@ -1,3 +1,7 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, +}; use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use asp::keys::{AspKey, AspKeyType}; @@ -68,16 +72,18 @@ impl AspmSubcommand for KeysGenerateCommand { 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")?; + let Ok(cipher_text) = Aes256Gcm::new(&Key::::from_slice(aes_key)) + .encrypt((&aes_key[0..12]).into(), key.jwk.to_string().as_bytes()) + else { + bail!("Failure encrypting key") + }; // Write to db let entry = keys::ActiveModel { fingerprint: ActiveValue::Set(key.fingerprint.clone()), key_type: ActiveValue::Set(key.key_type.into()), alias: ActiveValue::Set(alias), - encrypted: ActiveValue::Set(encrypted), + cipher_text: ActiveValue::Set(cipher_text), }; let res = keys::Entity::insert(entry) .exec(&state.db) diff --git a/src/commands/keys/import/gpg.rs b/src/commands/keys/import/gpg.rs index b449371..6b66dc9 100644 --- a/src/commands/keys/import/gpg.rs +++ b/src/commands/keys/import/gpg.rs @@ -2,6 +2,10 @@ use std::{io::Write, sync::Arc}; +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, +}; use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use clap::Parser; @@ -251,13 +255,18 @@ impl AspmSubcommand for KeysImportGpgCommand { let aes_key = hash.hash.context("Unable to derive encryption key")?; let aes_key = &aes_key.as_bytes()[0..32]; - let encrypted = asp_key.export_encrypted(aes_key)?; + let key = Key::::from_slice(aes_key); + let Ok(cipher_text) = Aes256Gcm::new(&key) + .encrypt((&aes_key[0..12]).into(), asp_key.jwk.to_string().as_bytes()) + else { + bail!("Failure encrypting key") + }; let entry = keys::ActiveModel { fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()), key_type: ActiveValue::Set(asp_key.key_type.clone().into()), alias: ActiveValue::Set(format!("{uid}", uid = uid.id.id())), - encrypted: ActiveValue::Set(encrypted), + cipher_text: ActiveValue::Set(cipher_text), }; // Because GPGME's context object is not 'Send', normal .await can't be used here, so this just blocks as a workaround let res = runtime diff --git a/src/commands/keys/import/jwk.rs b/src/commands/keys/import/jwk.rs index 9347d57..0661ae2 100644 --- a/src/commands/keys/import/jwk.rs +++ b/src/commands/keys/import/jwk.rs @@ -2,11 +2,16 @@ use std::io::Read; use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; -use asp::keys::{AspKey, AspKeyError}; +use asp::keys::AspKey; use clap::{Parser, ValueEnum}; use clap_stdin::FileOrStdin; use data_encoding::BASE64_NOPAD; use dialoguer::{theme::ColorfulTheme, Input, Password}; + +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, +}; use indoc::printdoc; use josekit::jwk::Jwk; use sea_orm::{ActiveValue, EntityTrait}; @@ -53,31 +58,20 @@ impl AspmSubcommand for KeysImportJwkCommand { .context("Unable to prompt on stderr") })?; - let key = match AspKey::from_jwk( + let asp_key = AspKey::from_jwk( Jwk::from_bytes({ let mut buf = Vec::new(); self.key.into_reader()?.read_to_end(&mut buf)?; buf - }).context("Unable to parse provided JWK")?, + }) + .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), - }, - }; + .context("Unable to convert parsed JWK to an AspKey")?; - let argon_salt = - SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes())) - .context("Unable to derive argon2 salt")?; + let argon_salt = SaltString::from_b64( + &BASE64_NOPAD.encode(asp_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) @@ -85,22 +79,25 @@ impl AspmSubcommand for KeysImportJwkCommand { 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")?; + let key = Key::::from_slice(aes_key); + let Ok(cipher_text) = Aes256Gcm::new(&key) + .encrypt((&aes_key[0..12]).into(), asp_key.jwk.to_string().as_bytes()) + else { + bail!("Failure encrypting key") + }; // Write to db let entry = keys::ActiveModel { - fingerprint: ActiveValue::Set(key.fingerprint.clone()), - key_type: ActiveValue::Set(key.key_type.clone().into()), + fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()), + key_type: ActiveValue::Set(asp_key.key_type.clone().into()), alias: ActiveValue::Set(alias), - encrypted: ActiveValue::Set(encrypted), + cipher_text: ActiveValue::Set(cipher_text), }; let res = keys::Entity::insert(entry) .exec(&state.db) .await .context("Unable to add key to database")?; - if res.last_insert_id != key.fingerprint { + if res.last_insert_id != asp_key.fingerprint { bail!("The key was unable to be saved to the database") } @@ -110,8 +107,8 @@ impl AspmSubcommand for KeysImportJwkCommand { Fingerprint: {fpr} Type: {type:?} ", - fpr = key.fingerprint, - r#type = key.key_type + fpr = asp_key.fingerprint, + r#type = asp_key.key_type }; Ok(()) diff --git a/src/entities/keys.rs b/src/entities/keys.rs index fb7a364..b9d2eb1 100644 --- a/src/entities/keys.rs +++ b/src/entities/keys.rs @@ -9,7 +9,7 @@ pub struct Model { pub fingerprint: String, pub key_type: i32, pub alias: String, - pub encrypted: String, + pub cipher_text: Vec, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/migrations/m_20230701_000001_create_keys_table.rs b/src/migrations/m_20230701_000001_create_keys_table.rs index 521b811..56931af 100644 --- a/src/migrations/m_20230701_000001_create_keys_table.rs +++ b/src/migrations/m_20230701_000001_create_keys_table.rs @@ -10,7 +10,7 @@ impl MigrationName for Migration { #[async_trait::async_trait] impl MigrationTrait for Migration { - // Define how to apply this migration: Create the Bakery table. + // Define how to apply this migration: Create the Keys table. async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .create_table( @@ -24,13 +24,13 @@ impl MigrationTrait for Migration { ) .col(ColumnDef::new(Keys::KeyType).integer().not_null()) .col(ColumnDef::new(Keys::Alias).string().not_null()) - .col(ColumnDef::new(Keys::Encrypted).string().not_null()) + .col(ColumnDef::new(Keys::CipherText).binary().not_null()) .to_owned(), ) .await } - // Define how to rollback this migration: Drop the Bakery table. + // Define how to rollback this migration: Drop the Keys table. async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Keys::Table).to_owned()) @@ -44,5 +44,5 @@ enum Keys { Fingerprint, KeyType, Alias, - Encrypted, + CipherText, }