use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
use crate::domain::errors::AuthError;
#[derive(Clone)]
pub enum SigningKey {
Hmac(String),
Rs256 {
private_pem: String,
public_pem: String,
},
Es256 {
private_pem: String,
public_pem: String,
},
}
impl std::fmt::Debug for SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SigningKey::Hmac(_) => write!(f, "SigningKey::Hmac(<redacted>)"),
SigningKey::Rs256 { .. } => write!(f, "SigningKey::Rs256(<pem>)"),
SigningKey::Es256 { .. } => write!(f, "SigningKey::Es256(<pem>)"),
}
}
}
impl SigningKey {
pub fn hmac(secret: impl Into<String>) -> Self {
SigningKey::Hmac(secret.into())
}
pub fn rs256(private_pem: impl Into<String>, public_pem: impl Into<String>) -> Self {
SigningKey::Rs256 { private_pem: private_pem.into(), public_pem: public_pem.into() }
}
pub fn es256(private_pem: impl Into<String>, public_pem: impl Into<String>) -> Self {
SigningKey::Es256 { private_pem: private_pem.into(), public_pem: public_pem.into() }
}
pub fn algorithm(&self) -> Algorithm {
match self {
SigningKey::Hmac(_) => Algorithm::HS256,
SigningKey::Rs256 { .. } => Algorithm::RS256,
SigningKey::Es256 { .. } => Algorithm::ES256,
}
}
pub(crate) fn encoding_key(&self) -> Result<EncodingKey, AuthError> {
match self {
SigningKey::Hmac(secret) => Ok(EncodingKey::from_secret(secret.as_bytes())),
SigningKey::Rs256 { private_pem, .. } => {
EncodingKey::from_rsa_pem(private_pem.as_bytes())
.map_err(|e| AuthError::TokenGeneration(format!("RS256 key error: {e}")))
}
SigningKey::Es256 { private_pem, .. } => {
EncodingKey::from_ec_pem(private_pem.as_bytes())
.map_err(|e| AuthError::TokenGeneration(format!("ES256 key error: {e}")))
}
}
}
pub(crate) fn decoding_key(&self) -> Result<DecodingKey, AuthError> {
match self {
SigningKey::Hmac(secret) => Ok(DecodingKey::from_secret(secret.as_bytes())),
SigningKey::Rs256 { public_pem, .. } => {
DecodingKey::from_rsa_pem(public_pem.as_bytes())
.map_err(|e| AuthError::TokenVerification(format!("RS256 key error: {e}")))
}
SigningKey::Es256 { public_pem, .. } => DecodingKey::from_ec_pem(public_pem.as_bytes())
.map_err(|e| AuthError::TokenVerification(format!("ES256 key error: {e}"))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::Algorithm;
#[test]
fn signing_key_algorithm_is_bound_to_variant() {
let hmac = SigningKey::hmac("secret");
let rs256 = SigningKey::rs256("private", "public");
let es256 = SigningKey::es256("private", "public");
assert_eq!(hmac.algorithm(), Algorithm::HS256);
assert_eq!(rs256.algorithm(), Algorithm::RS256);
assert_eq!(es256.algorithm(), Algorithm::ES256);
}
#[test]
fn signing_key_debug_redacts_hmac_secret() {
let key = SigningKey::hmac("s3cr3t");
assert_eq!(format!("{:?}", key), "SigningKey::Hmac(<redacted>)");
}
#[test]
fn signing_key_invalid_rs256_keys_fail_fast() {
let key = SigningKey::rs256("not-a-private-key", "not-a-public-key");
assert!(matches!(key.encoding_key(), Err(AuthError::TokenGeneration(_))));
assert!(matches!(key.decoding_key(), Err(AuthError::TokenVerification(_))));
}
#[test]
fn signing_key_invalid_es256_keys_fail_fast() {
let key = SigningKey::es256("not-a-private-key", "not-a-public-key");
assert!(matches!(key.encoding_key(), Err(AuthError::TokenGeneration(_))));
assert!(matches!(key.decoding_key(), Err(AuthError::TokenVerification(_))));
}
}