use crate::{
error::{Error, ErrorKind},
jwk::{Jwk, OctKey, PrivateJwk, SymmetricJwk},
secrets::{SecretBytes, SecretMap, SecretString},
};
#[derive(Debug, Clone, Copy, Default)]
pub struct JwkJson;
impl SecretMap for JwkJson {
type In = SecretString;
type Out = PrivateJwk;
fn apply(&self, input: SecretString) -> Result<PrivateJwk, Error> {
let jwk: Jwk = serde_json::from_str(input.expose_secret()).map_err(|source| {
Error::new(ErrorKind::Config, source).with_context("parsing JWK JSON")
})?;
jwk.private_jwk().ok_or_else(|| {
Error::from(ErrorKind::Config).with_context("JWK JSON contains no secret key material")
})
}
}
#[derive(Debug, Clone)]
pub struct OctBytes {
algorithm: String,
kid: Option<String>,
}
impl OctBytes {
#[must_use]
pub fn new(algorithm: impl Into<String>) -> Self {
Self {
algorithm: algorithm.into(),
kid: None,
}
}
#[must_use]
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
}
impl SecretMap for OctBytes {
type In = SecretBytes;
type Out = PrivateJwk;
fn apply(&self, input: SecretBytes) -> Result<PrivateJwk, Error> {
Ok(PrivateJwk::Symmetric(
SymmetricJwk::builder()
.key(
OctKey::builder()
.k(input.expose_secret().iter().copied())
.build(),
)
.algorithm(self.algorithm.clone())
.maybe_kid(self.kid.clone())
.build(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secrets::{ProvidedSecret, Secret as _};
const PRIVATE_JWK: &str = r#"{
"kty": "EC",
"crv": "P-256",
"alg": "ES256",
"kid": "in-the-jwk",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"
}"#;
#[tokio::test]
async fn parses_a_private_jwk_and_keeps_its_kid_and_alg() {
let jwk = ProvidedSecret::new(SecretString::new(PRIVATE_JWK))
.mapped(JwkJson)
.get_secret_value()
.await
.unwrap()
.value;
assert_eq!(jwk.kid(), Some("in-the-jwk"));
let PrivateJwk::Asymmetric(jwk) = jwk else {
panic!("an EC private JWK must parse as the Asymmetric variant");
};
assert_eq!(jwk.algorithm.as_deref(), Some("ES256"));
}
#[test]
fn parses_an_oct_jwk_as_symmetric() {
let oct = r#"{"kty":"oct","alg":"HS256","kid":"sym-1",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"}"#;
let jwk = JwkJson.apply(SecretString::new(oct)).unwrap();
let PrivateJwk::Symmetric(jwk) = jwk else {
panic!("an oct JWK must parse as the Symmetric variant");
};
assert_eq!(jwk.algorithm.as_deref(), Some("HS256"));
assert_eq!(jwk.kid.as_deref(), Some("sym-1"));
assert_eq!(jwk.key.k.len(), 64);
}
#[test]
fn rejects_json_without_private_material() {
let public_only = r#"{"kty":"EC","crv":"P-256","alg":"ES256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}"#;
let err = JwkJson.apply(SecretString::new(public_only)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
}
#[test]
fn rejects_malformed_json() {
let err = JwkJson.apply(SecretString::new("not json")).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Config);
}
#[test]
fn oct_bytes_stamps_alg_and_kid() {
let jwk = OctBytes::new("A256GCM")
.with_kid("cookie-key")
.apply(SecretBytes::new(vec![7u8; 32]))
.unwrap();
let PrivateJwk::Symmetric(jwk) = jwk else {
panic!("OctBytes must produce the Symmetric variant");
};
assert_eq!(jwk.algorithm.as_deref(), Some("A256GCM"));
assert_eq!(jwk.kid.as_deref(), Some("cookie-key"));
assert_eq!(jwk.key.k, vec![7u8; 32]);
}
#[test]
fn oct_bytes_without_kid_leaves_it_empty() {
let jwk = OctBytes::new("HS256")
.apply(SecretBytes::new(vec![1u8; 32]))
.unwrap();
assert_eq!(jwk.kid(), None);
}
}