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_jwk::srp::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(
96 dead_code,
97 reason = "reserved for future variant-based negative fixtures"
98 )]
99 fn load_variant(&self, variant: &str) -> Arc<Inner> {
100 load_inner(&self.factory, &self.label, self.spec, variant)
101 }
102
103 pub fn spec(&self) -> HmacSpec {
115 self.spec
116 }
117
118 pub fn label(&self) -> &str {
130 &self.label
131 }
132
133 pub fn secret_bytes(&self) -> &[u8] {
148 &self.inner.secret
149 }
150
151 #[cfg(feature = "jwk")]
164 pub fn kid(&self) -> String {
165 kid_from_bytes(self.secret_bytes())
166 }
167
168 #[cfg(feature = "jwk")]
185 pub fn jwk(&self) -> uselesskey_jwk::PrivateJwk {
186 use base64::Engine as _;
187 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
188 use uselesskey_jwk::{OctJwk, PrivateJwk};
189
190 let k = URL_SAFE_NO_PAD.encode(self.secret_bytes());
191
192 PrivateJwk::Oct(OctJwk {
193 kty: "oct",
194 use_: "sig",
195 alg: self.spec.alg_name(),
196 kid: self.kid(),
197 k,
198 })
199 }
200
201 #[cfg(feature = "jwk")]
217 pub fn jwks(&self) -> uselesskey_jwk::Jwks {
218 use uselesskey_jwk::JwksBuilder;
219
220 let mut builder = JwksBuilder::new();
221 builder.push_private(self.jwk());
222 builder.build()
223 }
224}
225
226fn load_inner(factory: &Factory, label: &str, spec: HmacSpec, variant: &str) -> Arc<Inner> {
227 let spec_bytes = spec.stable_bytes();
228
229 factory.get_or_init(DOMAIN_HMAC_SECRET, label, &spec_bytes, variant, |seed| {
230 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
231 let mut buf = vec![0u8; spec.byte_len()];
232 rng.fill_bytes(&mut buf);
233 Inner {
234 secret: Arc::from(buf),
235 }
236 })
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use uselesskey_core::Seed;
243
244 #[test]
245 fn secret_length_matches_spec() {
246 let fx = Factory::random();
247 let secret = fx.hmac("test", HmacSpec::hs256());
248 assert_eq!(secret.secret_bytes().len(), 32);
249 }
250
251 #[test]
252 fn deterministic_secret_is_stable() {
253 let fx = Factory::deterministic(Seed::from_env_value("hmac-seed").unwrap());
254 let s1 = fx.hmac("issuer", HmacSpec::hs384());
255 let s2 = fx.hmac("issuer", HmacSpec::hs384());
256 assert_eq!(s1.secret_bytes(), s2.secret_bytes());
257 }
258
259 #[test]
260 fn different_variants_produce_different_secrets() {
261 let fx = Factory::deterministic(Seed::from_env_value("hmac-variant").unwrap());
262 let secret = fx.hmac("issuer", HmacSpec::hs256());
263 let other = secret.load_variant("other");
264
265 assert_ne!(secret.secret_bytes(), other.secret.as_ref());
266 }
267
268 #[test]
269 #[cfg(feature = "jwk")]
270 fn jwk_contains_expected_fields() {
271 let fx = Factory::random();
272 let secret = fx.hmac("jwt", HmacSpec::hs512());
273 let jwk = secret.jwk().to_value();
274
275 assert_eq!(jwk["kty"], "oct");
276 assert_eq!(jwk["alg"], "HS512");
277 assert_eq!(jwk["use"], "sig");
278 assert!(jwk["kid"].is_string());
279 assert!(jwk["k"].is_string());
280 }
281
282 #[test]
283 #[cfg(feature = "jwk")]
284 fn jwk_k_is_base64url() {
285 use base64::Engine as _;
286 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
287
288 let fx = Factory::random();
289 let secret = fx.hmac("jwt", HmacSpec::hs256());
290 let jwk = secret.jwk().to_value();
291
292 let k = jwk["k"].as_str().unwrap();
293 let decoded = URL_SAFE_NO_PAD.decode(k).expect("valid base64url");
294 assert_eq!(decoded.len(), HmacSpec::hs256().byte_len());
295 }
296
297 #[test]
298 #[cfg(feature = "jwk")]
299 fn jwks_wraps_jwk() {
300 let fx = Factory::random();
301 let secret = fx.hmac("jwt", HmacSpec::hs256());
302
303 let jwk = secret.jwk().to_value();
304 let jwks = secret.jwks().to_value();
305
306 let keys = jwks["keys"].as_array().expect("keys array");
307 assert_eq!(keys.len(), 1);
308 assert_eq!(keys[0], jwk);
309 }
310
311 #[test]
312 #[cfg(feature = "jwk")]
313 fn kid_is_deterministic() {
314 let fx = Factory::deterministic(Seed::from_env_value("hmac-kid").unwrap());
315 let s1 = fx.hmac("issuer", HmacSpec::hs512());
316 let s2 = fx.hmac("issuer", HmacSpec::hs512());
317 assert_eq!(s1.kid(), s2.kid());
318 }
319
320 #[test]
321 #[cfg(feature = "jwk")]
322 fn kid_is_not_placeholder_for_any_spec() {
323 let fx = Factory::random();
324
325 for spec in [HmacSpec::hs256(), HmacSpec::hs384(), HmacSpec::hs512()] {
326 let secret = fx.hmac("kid-placeholder", spec);
327 assert_ne!(secret.kid(), "xyzzy");
328 }
329 }
330
331 #[test]
332 fn debug_includes_label_and_type() {
333 let fx = Factory::random();
334 let secret = fx.hmac("debug-label", HmacSpec::hs256());
335
336 let dbg = format!("{:?}", secret);
337 assert!(dbg.contains("HmacSecret"));
338 assert!(dbg.contains("debug-label"));
339 }
340}