mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 20:39:29 -07:00
Remove JWE formatting
This commit is contained in:
parent
620a8632bb
commit
424d3ce4fc
12 changed files with 244 additions and 254 deletions
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,50 +53,107 @@ pub struct 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() {
|
||||
"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, AspKeyError> {
|
||||
Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?)
|
||||
pub fn from_pkcs8(key: &str) -> Result<Self, anyhow::Error> {
|
||||
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> {
|
||||
self.jwk
|
||||
.to_pkcs8()
|
||||
.or(Err(AspKeyError::Pkcs8ConversionError))
|
||||
pub fn into_pkcs8(&self) -> Result<String, anyhow::Error> {
|
||||
// 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.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> {
|
||||
(|| -> anyhow::Result<Self> {
|
||||
pub fn generate(key_type: AspKeyType) -> Result<Self, anyhow::Error> {
|
||||
match key_type {
|
||||
AspKeyType::Ed25519 => {
|
||||
let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?;
|
||||
Ok(Self {
|
||||
key_type,
|
||||
fingerprint: jwk.get_fingerprint()?,
|
||||
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||
jwk,
|
||||
})
|
||||
}
|
||||
|
@ -100,13 +161,11 @@ impl AspKey {
|
|||
let jwk = Jwk::generate_ec_key(EcCurve::P256)?;
|
||||
Ok(Self {
|
||||
key_type,
|
||||
fingerprint: jwk.get_fingerprint()?,
|
||||
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||
jwk,
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
.or(Err(AspKeyError::GenerationError))
|
||||
}
|
||||
|
||||
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)?),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
type Error = AspKeyError;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: Jwk) -> Result<Self, Self::Error> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
pub mod jwk;
|
||||
pub mod jwt;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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::<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 {
|
||||
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")?,
|
||||
|
|
|
@ -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::<Aes256Gcm>::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)
|
||||
|
|
|
@ -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::<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 {
|
||||
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
|
||||
|
|
|
@ -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,30 +58,19 @@ 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::<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),
|
||||
},
|
||||
};
|
||||
.context("Unable to convert parsed JWK to an AspKey")?;
|
||||
|
||||
let argon_salt =
|
||||
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes()))
|
||||
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
|
||||
|
@ -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::<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")
|
||||
};
|
||||
|
||||
// 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(())
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct Model {
|
|||
pub fingerprint: String,
|
||||
pub key_type: i32,
|
||||
pub alias: String,
|
||||
pub encrypted: String,
|
||||
pub cipher_text: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue