uselesskey_hmac/
secret.rs1use std::fmt;
2use std::sync::Arc;
3
4use rand_chacha10::ChaCha20Rng;
5use rand_core10::{Rng, SeedableRng};
6use uselesskey_core::Factory;
7#[cfg(feature = "jwk")]
8use uselesskey_core_kid::kid_from_bytes;
9
10use crate::HmacSpec;
11
12pub const DOMAIN_HMAC_SECRET: &str = "uselesskey:hmac:secret";
16
17#[derive(Clone)]
34pub struct HmacSecret {
35 factory: Factory,
36 label: String,
37 spec: HmacSpec,
38 inner: Arc<Inner>,
39}
40
41struct Inner {
42 secret: Arc<[u8]>,
43}
44
45impl fmt::Debug for HmacSecret {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.debug_struct("HmacSecret")
48 .field("label", &self.label)
49 .field("spec", &self.spec)
50 .finish_non_exhaustive()
51 }
52}
53
54pub trait HmacFactoryExt {
56 fn hmac(&self, label: impl AsRef<str>, spec: HmacSpec) -> HmacSecret;
76}
77
78impl HmacFactoryExt for Factory {
79 fn hmac(&self, label: impl AsRef<str>, spec: HmacSpec) -> HmacSecret {
80 HmacSecret::new(self.clone(), label.as_ref(), spec)
81 }
82}
83
84impl HmacSecret {
85 fn new(factory: Factory, label: &str, spec: HmacSpec) -> Self {
86 let inner = load_inner(&factory, label, spec, "good");
87 Self {
88 factory,
89 label: label.to_string(),
90 spec,
91 inner,
92 }
93 }
94
95 #[allow(dead_code)]
96 fn load_variant(&self, variant: &str) -> Arc<Inner> {
97 load_inner(&self.factory, &self.label, self.spec, variant)
98 }
99
100 pub fn spec(&self) -> HmacSpec {
112 self.spec
113 }
114
115 pub fn label(&self) -> &str {
127 &self.label
128 }
129
130 pub fn secret_bytes(&self) -> &[u8] {
145 &self.inner.secret
146 }
147
148 #[cfg(feature = "jwk")]
161 pub fn kid(&self) -> String {
162 kid_from_bytes(self.secret_bytes())
163 }
164
165 #[cfg(feature = "jwk")]
182 pub fn jwk(&self) -> uselesskey_jwk::PrivateJwk {
183 use base64::Engine as _;
184 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
185 use uselesskey_jwk::{OctJwk, PrivateJwk};
186
187 let k = URL_SAFE_NO_PAD.encode(self.secret_bytes());
188
189 PrivateJwk::Oct(OctJwk {
190 kty: "oct",
191 use_: "sig",
192 alg: self.spec.alg_name(),
193 kid: self.kid(),
194 k,
195 })
196 }
197
198 #[cfg(feature = "jwk")]
214 pub fn jwks(&self) -> uselesskey_jwk::Jwks {
215 use uselesskey_jwk::JwksBuilder;
216
217 let mut builder = JwksBuilder::new();
218 builder.push_private(self.jwk());
219 builder.build()
220 }
221}
222
223fn load_inner(factory: &Factory, label: &str, spec: HmacSpec, variant: &str) -> Arc<Inner> {
224 let spec_bytes = spec.stable_bytes();
225
226 factory.get_or_init(DOMAIN_HMAC_SECRET, label, &spec_bytes, variant, |seed| {
227 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
228 let mut buf = vec![0u8; spec.byte_len()];
229 rng.fill_bytes(&mut buf);
230 Inner {
231 secret: Arc::from(buf),
232 }
233 })
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use uselesskey_core::Seed;
240
241 #[test]
242 fn secret_length_matches_spec() {
243 let fx = Factory::random();
244 let secret = fx.hmac("test", HmacSpec::hs256());
245 assert_eq!(secret.secret_bytes().len(), 32);
246 }
247
248 #[test]
249 fn deterministic_secret_is_stable() {
250 let fx = Factory::deterministic(Seed::from_env_value("hmac-seed").unwrap());
251 let s1 = fx.hmac("issuer", HmacSpec::hs384());
252 let s2 = fx.hmac("issuer", HmacSpec::hs384());
253 assert_eq!(s1.secret_bytes(), s2.secret_bytes());
254 }
255
256 #[test]
257 fn different_variants_produce_different_secrets() {
258 let fx = Factory::deterministic(Seed::from_env_value("hmac-variant").unwrap());
259 let secret = fx.hmac("issuer", HmacSpec::hs256());
260 let other = secret.load_variant("other");
261
262 assert_ne!(secret.secret_bytes(), other.secret.as_ref());
263 }
264
265 #[test]
266 #[cfg(feature = "jwk")]
267 fn jwk_contains_expected_fields() {
268 let fx = Factory::random();
269 let secret = fx.hmac("jwt", HmacSpec::hs512());
270 let jwk = secret.jwk().to_value();
271
272 assert_eq!(jwk["kty"], "oct");
273 assert_eq!(jwk["alg"], "HS512");
274 assert_eq!(jwk["use"], "sig");
275 assert!(jwk["kid"].is_string());
276 assert!(jwk["k"].is_string());
277 }
278
279 #[test]
280 #[cfg(feature = "jwk")]
281 fn jwk_k_is_base64url() {
282 use base64::Engine as _;
283 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
284
285 let fx = Factory::random();
286 let secret = fx.hmac("jwt", HmacSpec::hs256());
287 let jwk = secret.jwk().to_value();
288
289 let k = jwk["k"].as_str().unwrap();
290 let decoded = URL_SAFE_NO_PAD.decode(k).expect("valid base64url");
291 assert_eq!(decoded.len(), HmacSpec::hs256().byte_len());
292 }
293
294 #[test]
295 #[cfg(feature = "jwk")]
296 fn jwks_wraps_jwk() {
297 let fx = Factory::random();
298 let secret = fx.hmac("jwt", HmacSpec::hs256());
299
300 let jwk = secret.jwk().to_value();
301 let jwks = secret.jwks().to_value();
302
303 let keys = jwks["keys"].as_array().expect("keys array");
304 assert_eq!(keys.len(), 1);
305 assert_eq!(keys[0], jwk);
306 }
307
308 #[test]
309 #[cfg(feature = "jwk")]
310 fn kid_is_deterministic() {
311 let fx = Factory::deterministic(Seed::from_env_value("hmac-kid").unwrap());
312 let s1 = fx.hmac("issuer", HmacSpec::hs512());
313 let s2 = fx.hmac("issuer", HmacSpec::hs512());
314 assert_eq!(s1.kid(), s2.kid());
315 }
316
317 #[test]
318 #[cfg(feature = "jwk")]
319 fn kid_is_not_placeholder_for_any_spec() {
320 let fx = Factory::random();
321
322 for spec in [HmacSpec::hs256(), HmacSpec::hs384(), HmacSpec::hs512()] {
323 let secret = fx.hmac("kid-placeholder", spec);
324 assert_ne!(secret.kid(), "xyzzy");
325 }
326 }
327
328 #[test]
329 fn debug_includes_label_and_type() {
330 let fx = Factory::random();
331 let secret = fx.hmac("debug-label", HmacSpec::hs256());
332
333 let dbg = format!("{:?}", secret);
334 assert!(dbg.contains("HmacSecret"));
335 assert!(dbg.contains("debug-label"));
336 }
337}