uselesskey_hmac/
secret.rs1use std::fmt;
2use std::sync::Arc;
3
4use rand_core::RngCore;
5use uselesskey_core::Factory;
6#[cfg(feature = "jwk")]
7use uselesskey_core_kid::kid_from_bytes;
8
9use crate::HmacSpec;
10
11pub const DOMAIN_HMAC_SECRET: &str = "uselesskey:hmac:secret";
15
16#[derive(Clone)]
33pub struct HmacSecret {
34 factory: Factory,
35 label: String,
36 spec: HmacSpec,
37 inner: Arc<Inner>,
38}
39
40struct Inner {
41 secret: Arc<[u8]>,
42}
43
44impl fmt::Debug for HmacSecret {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_struct("HmacSecret")
47 .field("label", &self.label)
48 .field("spec", &self.spec)
49 .finish_non_exhaustive()
50 }
51}
52
53pub trait HmacFactoryExt {
55 fn hmac(&self, label: impl AsRef<str>, spec: HmacSpec) -> HmacSecret;
75}
76
77impl HmacFactoryExt for Factory {
78 fn hmac(&self, label: impl AsRef<str>, spec: HmacSpec) -> HmacSecret {
79 HmacSecret::new(self.clone(), label.as_ref(), spec)
80 }
81}
82
83impl HmacSecret {
84 fn new(factory: Factory, label: &str, spec: HmacSpec) -> Self {
85 let inner = load_inner(&factory, label, spec, "good");
86 Self {
87 factory,
88 label: label.to_string(),
89 spec,
90 inner,
91 }
92 }
93
94 #[allow(dead_code)]
95 fn load_variant(&self, variant: &str) -> Arc<Inner> {
96 load_inner(&self.factory, &self.label, self.spec, variant)
97 }
98
99 pub fn secret_bytes(&self) -> &[u8] {
114 &self.inner.secret
115 }
116
117 #[cfg(feature = "jwk")]
130 pub fn kid(&self) -> String {
131 kid_from_bytes(self.secret_bytes())
132 }
133
134 #[cfg(feature = "jwk")]
151 pub fn jwk(&self) -> uselesskey_jwk::PrivateJwk {
152 use base64::Engine as _;
153 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
154 use uselesskey_jwk::{OctJwk, PrivateJwk};
155
156 let k = URL_SAFE_NO_PAD.encode(self.secret_bytes());
157
158 PrivateJwk::Oct(OctJwk {
159 kty: "oct",
160 use_: "sig",
161 alg: self.spec.alg_name(),
162 kid: self.kid(),
163 k,
164 })
165 }
166
167 #[cfg(feature = "jwk")]
183 pub fn jwks(&self) -> uselesskey_jwk::Jwks {
184 use uselesskey_jwk::JwksBuilder;
185
186 let mut builder = JwksBuilder::new();
187 builder.push_private(self.jwk());
188 builder.build()
189 }
190}
191
192fn load_inner(factory: &Factory, label: &str, spec: HmacSpec, variant: &str) -> Arc<Inner> {
193 let spec_bytes = spec.stable_bytes();
194
195 factory.get_or_init(DOMAIN_HMAC_SECRET, label, &spec_bytes, variant, |rng| {
196 let mut buf = vec![0u8; spec.byte_len()];
197 rng.fill_bytes(&mut buf);
198 Inner {
199 secret: Arc::from(buf),
200 }
201 })
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use uselesskey_core::Seed;
208
209 #[test]
210 fn secret_length_matches_spec() {
211 let fx = Factory::random();
212 let secret = fx.hmac("test", HmacSpec::hs256());
213 assert_eq!(secret.secret_bytes().len(), 32);
214 }
215
216 #[test]
217 fn deterministic_secret_is_stable() {
218 let fx = Factory::deterministic(Seed::from_env_value("hmac-seed").unwrap());
219 let s1 = fx.hmac("issuer", HmacSpec::hs384());
220 let s2 = fx.hmac("issuer", HmacSpec::hs384());
221 assert_eq!(s1.secret_bytes(), s2.secret_bytes());
222 }
223
224 #[test]
225 fn different_variants_produce_different_secrets() {
226 let fx = Factory::deterministic(Seed::from_env_value("hmac-variant").unwrap());
227 let secret = fx.hmac("issuer", HmacSpec::hs256());
228 let other = secret.load_variant("other");
229
230 assert_ne!(secret.secret_bytes(), other.secret.as_ref());
231 }
232
233 #[test]
234 #[cfg(feature = "jwk")]
235 fn jwk_contains_expected_fields() {
236 let fx = Factory::random();
237 let secret = fx.hmac("jwt", HmacSpec::hs512());
238 let jwk = secret.jwk().to_value();
239
240 assert_eq!(jwk["kty"], "oct");
241 assert_eq!(jwk["alg"], "HS512");
242 assert_eq!(jwk["use"], "sig");
243 assert!(jwk["kid"].is_string());
244 assert!(jwk["k"].is_string());
245 }
246
247 #[test]
248 #[cfg(feature = "jwk")]
249 fn jwk_k_is_base64url() {
250 use base64::Engine as _;
251 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
252
253 let fx = Factory::random();
254 let secret = fx.hmac("jwt", HmacSpec::hs256());
255 let jwk = secret.jwk().to_value();
256
257 let k = jwk["k"].as_str().unwrap();
258 let decoded = URL_SAFE_NO_PAD.decode(k).expect("valid base64url");
259 assert_eq!(decoded.len(), HmacSpec::hs256().byte_len());
260 }
261
262 #[test]
263 #[cfg(feature = "jwk")]
264 fn jwks_wraps_jwk() {
265 let fx = Factory::random();
266 let secret = fx.hmac("jwt", HmacSpec::hs256());
267
268 let jwk = secret.jwk().to_value();
269 let jwks = secret.jwks().to_value();
270
271 let keys = jwks["keys"].as_array().expect("keys array");
272 assert_eq!(keys.len(), 1);
273 assert_eq!(keys[0], jwk);
274 }
275
276 #[test]
277 #[cfg(feature = "jwk")]
278 fn kid_is_deterministic() {
279 let fx = Factory::deterministic(Seed::from_env_value("hmac-kid").unwrap());
280 let s1 = fx.hmac("issuer", HmacSpec::hs512());
281 let s2 = fx.hmac("issuer", HmacSpec::hs512());
282 assert_eq!(s1.kid(), s2.kid());
283 }
284
285 #[test]
286 fn debug_includes_label_and_type() {
287 let fx = Factory::random();
288 let secret = fx.hmac("debug-label", HmacSpec::hs256());
289
290 let dbg = format!("{:?}", secret);
291 assert!(dbg.contains("HmacSecret"));
292 assert!(dbg.contains("debug-label"));
293 }
294}