diff --git a/Cargo.toml b/Cargo.toml index 891ebae..15fa5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ thiserror = "1" chrono = "0.4.23" sqlx = "0.5.13" url = "2.3.1" +serde = "1" +serde_json = "1" \ No newline at end of file diff --git a/crates/id/Cargo.toml b/crates/id/Cargo.toml index 1157e8b..e94ccff 100644 --- a/crates/id/Cargo.toml +++ b/crates/id/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" thiserror = { workspace = true } nanoid = "0.4.0" nanoid-dictionary = "0.4.3" -serde = { version = "1", features = ["derive"] } \ No newline at end of file +serde = { workspace = true, features = ["derive"] } diff --git a/crates/id/src/key.rs b/crates/id/src/key.rs new file mode 100644 index 0000000..e9f0e2b --- /dev/null +++ b/crates/id/src/key.rs @@ -0,0 +1,71 @@ +use super::Error; +use nanoid::nanoid; +use nanoid_dictionary::ALPHANUMERIC; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +const LENGTH: usize = 30; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct KeyID(pub String); + +impl Display for KeyID { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Default for KeyID { + fn default() -> Self { + Self(nanoid!(LENGTH, ALPHANUMERIC)) + } +} + +impl AsRef for KeyID { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl FromStr for KeyID { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.len() == LENGTH && s.chars().all(|c| ALPHANUMERIC.contains(&c)) { + Ok(Self(s.to_string())) + } else { + Err(Error::Invalid("Key")) + } + } +} + +#[cfg(test)] +mod tests { + use super::{KeyID, LENGTH}; + use std::str::FromStr; + + #[test] + fn invalid_length() { + assert!(KeyID::from_str("test").is_err()); + } + + #[test] + fn invalid_characters() { + let value = "abcdef!!!!!!@#$%^&*()_++zyxwvq"; + assert_eq!(value.len(), LENGTH); + + assert!(KeyID::from_str(value).is_err()); + } + + #[test] + fn valid() { + let value = "abcdefghijklmnopqrstuvwxyzabcd"; + assert_eq!(value.len(), LENGTH); + let id = KeyID::from_str(value); + assert!(id.is_ok()); + + let id = id.unwrap(); + assert_eq!(id.0, value); + } +} diff --git a/crates/id/src/lib.rs b/crates/id/src/lib.rs index 46e215d..c427f20 100644 --- a/crates/id/src/lib.rs +++ b/crates/id/src/lib.rs @@ -1,3 +1,4 @@ +mod key; mod user; // error @@ -9,4 +10,5 @@ pub enum Error { Invalid(&'static str), } +pub use key::KeyID; pub use user::UserID; diff --git a/crates/jwt/Cargo.toml b/crates/jwt/Cargo.toml new file mode 100644 index 0000000..1a2b7d4 --- /dev/null +++ b/crates/jwt/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "jwt" +version = "0.0.0" +edition = "2021" + +[dependencies] +jwt-compact = { version = "0.6.0", features = ["with_rsa"] } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +rand = "0.8.5" +rsa = "0.7.2" + +# local crates +id = { path = "../id" } diff --git a/crates/jwt/src/error.rs b/crates/jwt/src/error.rs new file mode 100644 index 0000000..ef1bf5f --- /dev/null +++ b/crates/jwt/src/error.rs @@ -0,0 +1,17 @@ +// error +#[derive(thiserror::Error)] +// the rest +#[derive(Debug)] +pub enum Error { + #[error("Failed to generate key: `{0}`")] + Generation(#[from] rsa::errors::Error), + + #[error("Failed to handle private key: `{0}`")] + Private(#[from] rsa::pkcs8::Error), + + #[error("Failed to handle public key: `{0}`")] + Public(#[from] rsa::pkcs1::Error), + + #[error("Failed to parse JWT: `{0}`")] + JwtParsing(#[from] jwt_compact::ParseError), +} diff --git a/crates/jwt/src/jwk.rs b/crates/jwt/src/jwk.rs new file mode 100644 index 0000000..279fb58 --- /dev/null +++ b/crates/jwt/src/jwk.rs @@ -0,0 +1,13 @@ +use jwt_compact::jwk::JsonWebKey as JsonWebKeyBase; +use serde::Serialize; +use std::borrow::Cow; + +#[derive(Debug, Serialize)] +pub struct JsonWebKey<'a> { + #[serde(flatten)] + pub base: JsonWebKeyBase<'a>, + #[serde(rename = "kid")] + pub key_id: &'a str, + #[serde(rename = "alg")] + pub algorithm: Cow<'a, str>, +} diff --git a/crates/jwt/src/key.rs b/crates/jwt/src/key.rs new file mode 100644 index 0000000..29f1365 --- /dev/null +++ b/crates/jwt/src/key.rs @@ -0,0 +1,19 @@ +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{ModulusBits, Rsa}; + +mod private; +mod public; + +pub use private::PrivateKey; +pub use public::PublicKey; + +pub fn generate(id: &KeyID) -> Result<(PrivateKey, PublicKey), Error> { + let mut rng = rand::thread_rng(); + let key_combo = Rsa::generate(&mut rng, ModulusBits::ThreeKibibytes)?; + + let private = PrivateKey::new(id, key_combo.0); + let public = PublicKey::new(id, key_combo.1); + + Ok((private, public)) +} diff --git a/crates/jwt/src/key/private.rs b/crates/jwt/src/key/private.rs new file mode 100644 index 0000000..c85bb62 --- /dev/null +++ b/crates/jwt/src/key/private.rs @@ -0,0 +1,66 @@ +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{RsaPrivateKey, StrongKey}; +use rsa::pkcs8::der::zeroize::Zeroizing; +use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey}; + +pub struct PrivateKey { + id: KeyID, + key: RsaPrivateKey, +} + +impl PrivateKey { + pub fn new(id: &KeyID, key: StrongKey) -> Self { + Self { + id: id.clone(), + key: key.into_inner(), + } + } + + pub fn to_der(&self) -> Result>, Error> { + Ok(self.key.to_pkcs8_der()?.to_bytes()) + } + + pub fn from_der(der: &[u8], id: &KeyID) -> Result { + Ok(Self { + id: id.clone(), + key: RsaPrivateKey::from_pkcs8_der(der)?, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::PrivateKey; + use id::KeyID; + + #[test] + fn generate() { + let key_id = KeyID::default(); + let keys = crate::generate(&key_id); + assert!(keys.is_ok()); + + let private_key = keys.unwrap().0; + assert_eq!(private_key.id, key_id); + } + + #[test] + fn import_export() { + let key_id = KeyID::default(); + // Import premade key + let premade_der = include_bytes!("../../tests/private_key.der"); + let private_key = PrivateKey::from_der(premade_der, &key_id); + + // Make sure import is ok + assert!(private_key.is_ok()); + let private_key = private_key.unwrap(); + + // Export private key + let exported_der = private_key.to_der(); + assert!(exported_der.is_ok()); + let exported_der = exported_der.unwrap(); + + // Make sure output matches + assert_eq!(premade_der.as_slice(), exported_der.as_slice()); + } +} diff --git a/crates/jwt/src/key/public.rs b/crates/jwt/src/key/public.rs new file mode 100644 index 0000000..1cf928e --- /dev/null +++ b/crates/jwt/src/key/public.rs @@ -0,0 +1,92 @@ +use crate::jwk::JsonWebKey; +use crate::Error; +use id::KeyID; +use jwt_compact::alg::{Rsa, RsaPublicKey, StrongKey}; +use jwt_compact::jwk::JsonWebKey as JsonWebKeyBase; +use jwt_compact::Algorithm; +use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; + +pub struct PublicKey { + id: KeyID, + key: RsaPublicKey, +} + +impl PublicKey { + pub fn new(id: &KeyID, key: StrongKey) -> Self { + Self { + id: id.clone(), + key: key.into_inner(), + } + } + + pub fn to_der(&self) -> Result, Error> { + Ok(self.key.to_pkcs1_der()?.to_vec()) + } + + pub fn from_der(der: &[u8], id: &KeyID) -> Result { + Ok(Self { + id: id.clone(), + key: RsaPublicKey::from_pkcs1_der(der)?, + }) + } + + pub fn jwk(&self) -> JsonWebKey { + JsonWebKey { + base: JsonWebKeyBase::from(&self.key), + key_id: self.id.as_ref(), + algorithm: Rsa::ps256().name(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::PublicKey; + use id::KeyID; + use std::str::FromStr; + + #[test] + fn generate() { + let key_id = KeyID::default(); + let keys = crate::generate(&key_id); + assert!(keys.is_ok()); + + let public_key = keys.unwrap().1; + assert_eq!(public_key.id, key_id); + } + + #[test] + fn import_export() { + let key_id = KeyID::default(); + // Import premade key + let premade_der = include_bytes!("../../tests/public_key.der"); + let public_key = PublicKey::from_der(premade_der, &key_id); + + // Make sure import is ok + assert!(public_key.is_ok()); + let public_key = public_key.unwrap(); + + // Export public key + let exported_der = public_key.to_der(); + assert!(exported_der.is_ok()); + let exported_der = exported_der.unwrap(); + + // Make sure output matches + assert_eq!(premade_der.as_slice(), exported_der.as_slice()); + } + + #[test] + fn jwk() { + let key_id = KeyID::from_str("SgTG8ulMHAp5UsGWuCclw36zWsdEo5").unwrap(); + // Import premade key + let premade_der = include_bytes!("../../tests/public_key.der"); + let public_key = PublicKey::from_der(premade_der, &key_id).unwrap(); + + let generated_jwk = serde_json::to_string(&public_key.jwk()); + assert!(generated_jwk.is_ok()); + + let jwk = r#"{"kty":"RSA","n":"3os0j_kNfdTHJVQ-eMYXyRBWIqsrJDdELxLAh3_WlOZtsBwiGVNnpHQm9cRB63Un9UJpYGbWz38emglXc8bHPrUArDl-K_5ioDlbh7hAaz3rZ6b8LDIPUO-jYICdxBdv1THXSWbTEistZF1TYsXg7G4xrxiFKnZLBNaSgJrKOAY8AUNWuby-vZKr5X9e3SG7kvPsUITyqSmDz4ZTCj4QScx4O9gyqz1_UEBxTRSKcpS82YzAo2Byo5avRWesiGoaxs8lNv0QJ22IY1KVoROv3hHFeFEcg3D4NTfFG2Cd8d1OMXfILhtFnQZbt5ZxIG9SCOfirn32-9OtoLemKlgSq0gbLf6t1OK12LK6mIJ78pphlnhHdvHeJ75PV6c2lS2Wwd75NYBJzhIojG4U4Lbpe7T_NDFaxExry_7V5oxX8tbb-OzJnuPOQRR0H5uOBjdVo7i5vjnDKOTDpro3XPQjBbIBkABhDdU2FcXkEbl8_ByyYZZni7ekzGrVSJB_vxvv","e":"AQAB","kid":"SgTG8ulMHAp5UsGWuCclw36zWsdEo5","alg":"PS256"}"#; + let generated_jwk = generated_jwk.unwrap(); + assert_eq!(jwk, generated_jwk); + } +} diff --git a/crates/jwt/src/lib.rs b/crates/jwt/src/lib.rs new file mode 100644 index 0000000..b6241fa --- /dev/null +++ b/crates/jwt/src/lib.rs @@ -0,0 +1,11 @@ +extern crate core; + +mod error; +mod jwk; +mod key; +mod token; + +/// Exports +pub use error::Error; +pub use key::generate; +pub use key::{PrivateKey, PublicKey}; diff --git a/crates/jwt/src/token.rs b/crates/jwt/src/token.rs new file mode 100644 index 0000000..a683fc6 --- /dev/null +++ b/crates/jwt/src/token.rs @@ -0,0 +1,25 @@ +use crate::Error; +use jwt_compact::UntrustedToken; + +pub fn parse(token: &str) -> Result { + Ok(UntrustedToken::new(token)?) +} + +#[cfg(test)] +mod tests { + #[test] + fn parse_valid() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + let token = super::parse(jwt); + + assert!(token.is_ok()); + } + + #[test] + fn parse_invalid() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let token = super::parse(jwt); + + assert!(token.is_err()); + } +} diff --git a/crates/jwt/tests/private_key.der b/crates/jwt/tests/private_key.der new file mode 100644 index 0000000..ff0c311 Binary files /dev/null and b/crates/jwt/tests/private_key.der differ diff --git a/crates/jwt/tests/public_key.der b/crates/jwt/tests/public_key.der new file mode 100644 index 0000000..1fc360f Binary files /dev/null and b/crates/jwt/tests/public_key.der differ