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 secret_bytes(&self) -> &[u8] {
115 &self.inner.secret
116 }
117
118 #[cfg(feature = "jwk")]
131 pub fn kid(&self) -> String {
132 kid_from_bytes(self.secret_bytes())
133 }
134
135 #[cfg(feature = "jwk")]
152 pub fn jwk(&self) -> uselesskey_jwk::PrivateJwk {
153 use base64::Engine as _;
154 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
155 use uselesskey_jwk::{OctJwk, PrivateJwk};
156
157 let k = URL_SAFE_NO_PAD.encode(self.secret_bytes());
158
159 PrivateJwk::Oct(OctJwk {
160 kty: "oct",
161 use_: "sig",
162 alg: self.spec.alg_name(),
163 kid: self.kid(),
164 k,
165 })
166 }
167
168 #[cfg(feature = "jwk")]
184 pub fn jwks(&self) -> uselesskey_jwk::Jwks {
185 use uselesskey_jwk::JwksBuilder;
186
187 let mut builder = JwksBuilder::new();
188 builder.push_private(self.jwk());
189 builder.build()
190 }
191}
192
193fn load_inner(factory: &Factory, label: &str, spec: HmacSpec, variant: &str) -> Arc<Inner> {
194 let spec_bytes = spec.stable_bytes();
195
196 factory.get_or_init(DOMAIN_HMAC_SECRET, label, &spec_bytes, variant, |seed| {
197 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
198 let mut buf = vec![0u8; spec.byte_len()];
199 rng.fill_bytes(&mut buf);
200 Inner {
201 secret: Arc::from(buf),
202 }
203 })
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use uselesskey_core::Seed;
210
211 #[test]
212 fn secret_length_matches_spec() {
213 let fx = Factory::random();
214 let secret = fx.hmac("test", HmacSpec::hs256());
215 assert_eq!(secret.secret_bytes().len(), 32);
216 }
217
218 #[test]
219 fn deterministic_secret_is_stable() {
220 let fx = Factory::deterministic(Seed::from_env_value("hmac-seed").unwrap());
221 let s1 = fx.hmac("issuer", HmacSpec::hs384());
222 let s2 = fx.hmac("issuer", HmacSpec::hs384());
223 assert_eq!(s1.secret_bytes(), s2.secret_bytes());
224 }
225
226 #[test]
227 fn different_variants_produce_different_secrets() {
228 let fx = Factory::deterministic(Seed::from_env_value("hmac-variant").unwrap());
229 let secret = fx.hmac("issuer", HmacSpec::hs256());
230 let other = secret.load_variant("other");
231
232 assert_ne!(secret.secret_bytes(), other.secret.as_ref());
233 }
234
235 #[test]
236 #[cfg(feature = "jwk")]
237 fn jwk_contains_expected_fields() {
238 let fx = Factory::random();
239 let secret = fx.hmac("jwt", HmacSpec::hs512());
240 let jwk = secret.jwk().to_value();
241
242 assert_eq!(jwk["kty"], "oct");
243 assert_eq!(jwk["alg"], "HS512");
244 assert_eq!(jwk["use"], "sig");
245 assert!(jwk["kid"].is_string());
246 assert!(jwk["k"].is_string());
247 }
248
249 #[test]
250 #[cfg(feature = "jwk")]
251 fn jwk_k_is_base64url() {
252 use base64::Engine as _;
253 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
254
255 let fx = Factory::random();
256 let secret = fx.hmac("jwt", HmacSpec::hs256());
257 let jwk = secret.jwk().to_value();
258
259 let k = jwk["k"].as_str().unwrap();
260 let decoded = URL_SAFE_NO_PAD.decode(k).expect("valid base64url");
261 assert_eq!(decoded.len(), HmacSpec::hs256().byte_len());
262 }
263
264 #[test]
265 #[cfg(feature = "jwk")]
266 fn jwks_wraps_jwk() {
267 let fx = Factory::random();
268 let secret = fx.hmac("jwt", HmacSpec::hs256());
269
270 let jwk = secret.jwk().to_value();
271 let jwks = secret.jwks().to_value();
272
273 let keys = jwks["keys"].as_array().expect("keys array");
274 assert_eq!(keys.len(), 1);
275 assert_eq!(keys[0], jwk);
276 }
277
278 #[test]
279 #[cfg(feature = "jwk")]
280 fn kid_is_deterministic() {
281 let fx = Factory::deterministic(Seed::from_env_value("hmac-kid").unwrap());
282 let s1 = fx.hmac("issuer", HmacSpec::hs512());
283 let s2 = fx.hmac("issuer", HmacSpec::hs512());
284 assert_eq!(s1.kid(), s2.kid());
285 }
286
287 #[test]
288 fn debug_includes_label_and_type() {
289 let fx = Factory::random();
290 let secret = fx.hmac("debug-label", HmacSpec::hs256());
291
292 let dbg = format!("{:?}", secret);
293 assert!(dbg.contains("HmacSecret"));
294 assert!(dbg.contains("debug-label"));
295 }
296}