1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-23 01:19:28 -07:00

Remove JWE formatting

This commit is contained in:
Tyler Beckman 2024-01-28 16:49:38 -07:00
parent 620a8632bb
commit 424d3ce4fc
Signed by: Ty
GPG key ID: 2813440C772555A4
12 changed files with 244 additions and 254 deletions

73
Cargo.lock generated
View file

@ -17,6 +17,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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]] [[package]]
name = "aes" name = "aes"
version = "0.8.3" version = "0.8.3"
@ -28,6 +38,20 @@ dependencies = [
"cpufeatures", "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]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.7" version = "0.7.7"
@ -195,6 +219,7 @@ dependencies = [
name = "aspm" name = "aspm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aes-gcm",
"anstyle", "anstyle",
"anyhow", "anyhow",
"app_dirs2", "app_dirs2",
@ -748,6 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
@ -761,6 +787,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "curve25519-dalek" name = "curve25519-dalek"
version = "4.1.1" version = "4.1.1"
@ -1258,6 +1293,16 @@ dependencies = [
"wasi", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.1" version = "0.28.1"
@ -1934,6 +1979,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.63" version = "0.10.63"
@ -2187,6 +2238,18 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" 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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -3587,6 +3650,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 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]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"

View file

@ -31,6 +31,7 @@ gpgme = { version = "0.11.0", optional = true }
pgp = { version = "0.10.2", optional = true } pgp = { version = "0.10.2", optional = true }
josekit = { version = "0.8.5", optional = true } josekit = { version = "0.8.5", optional = true }
elliptic-curve = { version = "0.13.8", optional = true } elliptic-curve = { version = "0.13.8", optional = true }
aes-gcm = "0.10.3"
[features] [features]
gpg-compat = ["dep:gpgme", "dep:pgp", "dep:josekit", "dep:elliptic-curve"] gpg-compat = ["dep:gpgme", "dep:pgp", "dep:josekit", "dep:elliptic-curve"]

View file

@ -1,17 +1,21 @@
use anyhow::bail; use anyhow::{bail, Context};
use data_encoding::{BASE32_NOPAD, BASE64_NOPAD};
use josekit::{ use josekit::{
jwk::{ jwk::{
alg::{ec::EcCurve, ed::EdCurve}, alg::{
Jwk, ec::{EcCurve, EcKeyPair},
ed::{EdCurve, EdKeyPair},
},
Jwk, KeyPair,
}, },
jws::{ jws::{
alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa}, alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa},
JwsHeader, JwsSigner, JwsVerifier, JwsHeader, JwsSigner, JwsVerifier, ES256,
}, },
}; };
use thiserror::Error; use openssl::pkey::PKey;
use serde_json::Map;
use crate::utils::jwk::JwtExt; use sha2::{Digest, Sha512};
/// An enum representing the possible types of JWK for ASPs /// An enum representing the possible types of JWK for ASPs
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -49,64 +53,119 @@ pub struct AspKey {
} }
impl AspKey { impl AspKey {
pub fn from_jwk(jwk: Jwk) -> Result<Self, AspKeyError> { /// Calculates the ASP fingerprint of an arbitrary [Jwk]
pub fn calculate_fingerprint(jwk: &Jwk) -> Result<String, anyhow::Error> {
// 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<u8> = {
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<Self, anyhow::Error> {
// Calculate the fingerprint
match jwk.key_type() { match jwk.key_type() {
"OKP" => match jwk.curve() { "OKP" => match jwk.curve() {
Some("Ed25519") => Ok(Self { Some("Ed25519") => Ok(Self {
key_type: AspKeyType::Ed25519, key_type: AspKeyType::Ed25519,
fingerprint: jwk fingerprint: Self::calculate_fingerprint(&jwk)?,
.get_fingerprint()
.or(Err(AspKeyError::FingerprintError))?,
jwk, jwk,
}), }),
_ => Err(AspKeyError::InvalidJwkType), _ => bail!("Invalid JWK type"),
}, },
"EC" => match jwk.curve() { "EC" => match jwk.curve() {
Some("P-256") => Ok(Self { Some("P-256") => Ok(Self {
key_type: AspKeyType::ES256, key_type: AspKeyType::ES256,
fingerprint: jwk fingerprint: Self::calculate_fingerprint(&jwk)?,
.get_fingerprint()
.or(Err(AspKeyError::FingerprintError))?,
jwk, jwk,
}), }),
_ => Err(AspKeyError::InvalidJwkType), _ => bail!("Invalid JWK type"),
}, },
_ => Err(AspKeyError::InvalidJwkType), _ => bail!("Invalid JWK type"),
} }
} }
pub fn from_pkcs8(key: &str) -> Result<Self, AspKeyError> { pub fn from_pkcs8(key: &str) -> Result<Self, anyhow::Error> {
Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?) 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<String, AspKeyError> { pub fn into_pkcs8(&self) -> Result<String, anyhow::Error> {
self.jwk // 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:
.to_pkcs8() // 1. Get the josekit EcKeyPair from the Jwk
.or(Err(AspKeyError::Pkcs8ConversionError)) // 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<dyn KeyPair> = 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<Self, AspKeyError> { pub fn generate(key_type: AspKeyType) -> Result<Self, anyhow::Error> {
(|| -> anyhow::Result<Self> { match key_type {
match key_type { AspKeyType::Ed25519 => {
AspKeyType::Ed25519 => { let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?;
let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?; Ok(Self {
Ok(Self { key_type,
key_type, fingerprint: Self::calculate_fingerprint(&jwk)?,
fingerprint: jwk.get_fingerprint()?, jwk,
jwk, })
})
}
AspKeyType::ES256 => {
let jwk = Jwk::generate_ec_key(EcCurve::P256)?;
Ok(Self {
key_type,
fingerprint: jwk.get_fingerprint()?,
jwk,
})
}
} }
})() AspKeyType::ES256 => {
.or(Err(AspKeyError::GenerationError)) 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<Box<dyn JwsSigner>> { pub fn create_signer(&self) -> anyhow::Result<Box<dyn JwsSigner>> {
@ -122,19 +181,10 @@ impl AspKey {
AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?), AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?),
}) })
} }
pub fn export_encrypted(&self, secret: &[u8]) -> anyhow::Result<String> {
self.jwk.encrypt(secret)
}
pub fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Self> {
let jwk = Jwk::from_encrypted(secret, jwe)?;
Ok(Self::from_jwk(jwk)?)
}
} }
impl TryFrom<Jwk> for AspKey { impl TryFrom<Jwk> for AspKey {
type Error = AspKeyError; type Error = anyhow::Error;
fn try_from(value: Jwk) -> Result<Self, Self::Error> { fn try_from(value: Jwk) -> Result<Self, Self::Error> {
Self::from_jwk(value) 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)] #[cfg(test)]
mod tests { mod tests {
use josekit::jwk::{ use josekit::jwk::{
@ -176,7 +214,7 @@ mod tests {
Jwk, Jwk,
}; };
use crate::keys::{AspKey, AspKeyError, AspKeyType}; use crate::keys::{AspKey, AspKeyType};
#[test] #[test]
fn generate_eddsa() { fn generate_eddsa() {
@ -239,31 +277,6 @@ mod tests {
let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type! let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type!
assert!(jwk.is_ok(), "jwk should generate successfully"); assert!(jwk.is_ok(), "jwk should generate successfully");
let key = AspKey::from_jwk(jwk.unwrap()); let key = AspKey::from_jwk(jwk.unwrap());
assert_eq!( assert!(key.is_err(), "key should fail to convert");
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());
} }
} }

View file

@ -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<String>;
fn to_pkcs8(&self) -> anyhow::Result<String>;
fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result<Jwk>;
fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Jwk>;
fn encrypt(&self, secret: &[u8]) -> anyhow::Result<String>;
}
impl JwtExt for Jwk {
fn get_fingerprint(&self) -> anyhow::Result<String> {
// 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<u8> = {
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<String> {
// 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<dyn KeyPair> = 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<Self> {
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<String> {
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<Jwk> {
let decrypter = A256gcmkw.decrypter_from_bytes(secret)?;
let (deserialized, _) = jwe::deserialize_compact(jwe, &decrypter)?;
let jwk = Jwk::from_bytes(deserialized)?;
Ok(jwk)
}
}

View file

@ -1,2 +1 @@
pub mod jwk;
pub mod jwt; pub mod jwt;

View file

@ -10,7 +10,7 @@ use std::io::Write;
use crate::{ use crate::{
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult}, commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
entities::prelude::* entities::prelude::*,
}; };
/// Deletes a saved key, after asking for confirmation. /// Deletes a saved key, after asking for confirmation.

View file

@ -1,3 +1,7 @@
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle}; use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
@ -6,18 +10,17 @@ use clap::{Parser, ValueEnum};
use data_encoding::BASE64_NOPAD; use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Password}; use dialoguer::{theme::ColorfulTheme, Password};
use indoc::writedoc; use indoc::writedoc;
use josekit::jwk::Jwk;
use std::io::Write; use std::io::Write;
use crate::{ use crate::{
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult}, commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
entities::prelude::* entities::prelude::*,
}; };
#[derive(ValueEnum, Debug, Clone)] #[derive(ValueEnum, Debug, Clone)]
pub enum KeyExportFormat { 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. /// An unencrypted PKCS#8 format. This is the format used by the asp.keyoxide.org web tool.
#[clap(alias = "PKCS#8")] #[clap(alias = "PKCS#8")]
PKCS8, PKCS8,
@ -83,11 +86,12 @@ impl AspmSubcommand for KeysExportCommand {
let aes_key = hash.hash.context("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 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::<Aes256Gcm>::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 { let export = match self.format {
KeyExportFormat::Encrypted => decrypted
.export_encrypted(aes_key)
.context("Unable to convert key into encrypted format")?,
KeyExportFormat::PKCS8 => decrypted KeyExportFormat::PKCS8 => decrypted
.into_pkcs8() .into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?, .context("Unable to convert key into PKCS#8 format")?,

View file

@ -1,3 +1,7 @@
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use anyhow::{anyhow, bail, Context}; use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use asp::keys::{AspKey, AspKeyType}; 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 = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32]; let aes_key = &aes_key.as_bytes()[0..32];
let encrypted = key let Ok(cipher_text) = Aes256Gcm::new(&Key::<Aes256Gcm>::from_slice(aes_key))
.export_encrypted(aes_key) .encrypt((&aes_key[0..12]).into(), key.jwk.to_string().as_bytes())
.context("Unable to derive the encryption key")?; else {
bail!("Failure encrypting key")
};
// Write to db // Write to db
let entry = keys::ActiveModel { let entry = keys::ActiveModel {
fingerprint: ActiveValue::Set(key.fingerprint.clone()), fingerprint: ActiveValue::Set(key.fingerprint.clone()),
key_type: ActiveValue::Set(key.key_type.into()), key_type: ActiveValue::Set(key.key_type.into()),
alias: ActiveValue::Set(alias), alias: ActiveValue::Set(alias),
encrypted: ActiveValue::Set(encrypted), cipher_text: ActiveValue::Set(cipher_text),
}; };
let res = keys::Entity::insert(entry) let res = keys::Entity::insert(entry)
.exec(&state.db) .exec(&state.db)

View file

@ -2,6 +2,10 @@
use std::{io::Write, sync::Arc}; use std::{io::Write, sync::Arc};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use anyhow::{anyhow, bail, Context}; use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use clap::Parser; 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 = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32]; let aes_key = &aes_key.as_bytes()[0..32];
let encrypted = asp_key.export_encrypted(aes_key)?; let key = Key::<Aes256Gcm>::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 { let entry = keys::ActiveModel {
fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()), fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()),
key_type: ActiveValue::Set(asp_key.key_type.clone().into()), key_type: ActiveValue::Set(asp_key.key_type.clone().into()),
alias: ActiveValue::Set(format!("{uid}", uid = uid.id.id())), 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 // 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 let res = runtime

View file

@ -2,11 +2,16 @@ use std::io::Read;
use anyhow::{anyhow, bail, Context}; use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use asp::keys::{AspKey, AspKeyError}; use asp::keys::AspKey;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use clap_stdin::FileOrStdin; use clap_stdin::FileOrStdin;
use data_encoding::BASE64_NOPAD; use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Input, Password}; use dialoguer::{theme::ColorfulTheme, Input, Password};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key,
};
use indoc::printdoc; use indoc::printdoc;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use sea_orm::{ActiveValue, EntityTrait}; use sea_orm::{ActiveValue, EntityTrait};
@ -53,31 +58,20 @@ impl AspmSubcommand for KeysImportJwkCommand {
.context("Unable to prompt on stderr") .context("Unable to prompt on stderr")
})?; })?;
let key = match AspKey::from_jwk( let asp_key = AspKey::from_jwk(
Jwk::from_bytes({ Jwk::from_bytes({
let mut buf = Vec::new(); let mut buf = Vec::new();
self.key.into_reader()?.read_to_end(&mut buf)?; self.key.into_reader()?.read_to_end(&mut buf)?;
buf buf
}).context("Unable to parse provided JWK")?, })
.context("Unable to parse provided JWK")?,
) )
.context("Unable to convert parsed JWK to an AspKey") .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 = let argon_salt = SaltString::from_b64(
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes())) &BASE64_NOPAD.encode(asp_key.fingerprint.to_uppercase().as_bytes()),
.context("Unable to derive argon2 salt")?; )
.context("Unable to derive argon2 salt")?;
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let hash = argon2 let hash = argon2
.hash_password(key_password.as_bytes(), &argon_salt) .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 = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32]; let aes_key = &aes_key.as_bytes()[0..32];
let encrypted = key let key = Key::<Aes256Gcm>::from_slice(aes_key);
.export_encrypted(aes_key) let Ok(cipher_text) = Aes256Gcm::new(&key)
.context("Unable to derive the encryption key")?; .encrypt((&aes_key[0..12]).into(), asp_key.jwk.to_string().as_bytes())
else {
bail!("Failure encrypting key")
};
// Write to db // Write to db
let entry = keys::ActiveModel { let entry = keys::ActiveModel {
fingerprint: ActiveValue::Set(key.fingerprint.clone()), fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()),
key_type: ActiveValue::Set(key.key_type.clone().into()), key_type: ActiveValue::Set(asp_key.key_type.clone().into()),
alias: ActiveValue::Set(alias), alias: ActiveValue::Set(alias),
encrypted: ActiveValue::Set(encrypted), cipher_text: ActiveValue::Set(cipher_text),
}; };
let res = keys::Entity::insert(entry) let res = keys::Entity::insert(entry)
.exec(&state.db) .exec(&state.db)
.await .await
.context("Unable to add key to database")?; .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") bail!("The key was unable to be saved to the database")
} }
@ -110,8 +107,8 @@ impl AspmSubcommand for KeysImportJwkCommand {
Fingerprint: {fpr} Fingerprint: {fpr}
Type: {type:?} Type: {type:?}
", ",
fpr = key.fingerprint, fpr = asp_key.fingerprint,
r#type = key.key_type r#type = asp_key.key_type
}; };
Ok(()) Ok(())

View file

@ -9,7 +9,7 @@ pub struct Model {
pub fingerprint: String, pub fingerprint: String,
pub key_type: i32, pub key_type: i32,
pub alias: String, pub alias: String,
pub encrypted: String, pub cipher_text: Vec<u8>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -10,7 +10,7 @@ impl MigrationName for Migration {
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigrationTrait for Migration { 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> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.create_table( .create_table(
@ -24,13 +24,13 @@ impl MigrationTrait for Migration {
) )
.col(ColumnDef::new(Keys::KeyType).integer().not_null()) .col(ColumnDef::new(Keys::KeyType).integer().not_null())
.col(ColumnDef::new(Keys::Alias).string().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(), .to_owned(),
) )
.await .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> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.drop_table(Table::drop().table(Keys::Table).to_owned()) .drop_table(Table::drop().table(Keys::Table).to_owned())
@ -44,5 +44,5 @@ enum Keys {
Fingerprint, Fingerprint,
KeyType, KeyType,
Alias, Alias,
Encrypted, CipherText,
} }