1#![forbid(unsafe_code)]
2#![cfg_attr(not(feature = "std"), no_std)]
3extern crate alloc;
10
11use alloc::string::String;
12use uselesskey_core_hash::Hasher;
13pub use uselesskey_core_hash::{hash32, write_len_prefixed};
14
15pub use uselesskey_core_seed::Seed;
16
17pub type ArtifactDomain = &'static str;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
22pub struct DerivationVersion(pub u16);
23
24impl DerivationVersion {
25 pub const V1: Self = Self(1);
27}
28
29#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
34pub struct ArtifactId {
35 pub domain: ArtifactDomain,
37 pub label: String,
39 pub spec_fingerprint: [u8; 32],
41 pub variant: String,
43 pub derivation_version: DerivationVersion,
45}
46
47impl ArtifactId {
48 pub fn new(
50 domain: ArtifactDomain,
51 label: impl Into<String>,
52 spec_bytes: &[u8],
53 variant: impl Into<String>,
54 derivation_version: DerivationVersion,
55 ) -> Self {
56 Self {
57 domain,
58 label: label.into(),
59 spec_fingerprint: *hash32(spec_bytes).as_bytes(),
60 variant: variant.into(),
61 derivation_version,
62 }
63 }
64}
65
66pub fn derive_seed(master: &Seed, id: &ArtifactId) -> Seed {
68 match id.derivation_version.0 {
69 1 => derive_seed_v1(master, id),
70 other => {
71 #[cfg(feature = "std")]
72 eprintln!("uselesskey-core-id: unknown derivation version {other}, using v1");
73 #[cfg(not(feature = "std"))]
74 let _ = other;
75 derive_seed_v1(master, id)
76 }
77 }
78}
79
80fn derive_seed_v1(master: &Seed, id: &ArtifactId) -> Seed {
81 let mut hasher = Hasher::new_keyed(master.bytes());
82
83 hasher.update(&id.derivation_version.0.to_be_bytes());
84 write_len_prefixed(&mut hasher, id.domain.as_bytes());
85 write_len_prefixed(&mut hasher, id.label.as_bytes());
86 write_len_prefixed(&mut hasher, id.variant.as_bytes());
87 hasher.update(&id.spec_fingerprint);
88
89 let out = hasher.finalize();
90 Seed::new(*out.as_bytes())
91}
92
93#[cfg(test)]
94mod tests {
95 use super::{ArtifactId, DerivationVersion, Seed, derive_seed, hash32};
96
97 #[test]
98 fn artifact_id_fingerprints_spec_bytes() {
99 let spec = [1u8, 2, 3, 4, 5];
100 let id = ArtifactId::new(
101 "domain:test",
102 "label",
103 &spec,
104 "variant",
105 DerivationVersion::V1,
106 );
107
108 let expected = *hash32(&spec).as_bytes();
109 assert_eq!(id.spec_fingerprint, expected);
110 }
111
112 #[test]
113 fn artifact_id_preserves_fields() {
114 let id = ArtifactId::new(
115 "domain:test",
116 "my-label",
117 b"spec",
118 "my-variant",
119 DerivationVersion::V1,
120 );
121
122 assert_eq!(id.domain, "domain:test");
123 assert_eq!(id.label, "my-label");
124 assert_eq!(id.variant, "my-variant");
125 assert_eq!(id.derivation_version, DerivationVersion::V1);
126 }
127
128 #[test]
129 fn derive_seed_unknown_version_is_deterministic() {
130 let master = Seed::new([9u8; 32]);
131 let id = ArtifactId::new(
132 "domain:test",
133 "label",
134 b"spec",
135 "variant",
136 DerivationVersion(999),
137 );
138
139 let first = derive_seed(&master, &id);
140 let second = derive_seed(&master, &id);
141 assert_eq!(first.bytes(), second.bytes());
142 }
143
144 #[test]
145 fn derive_seed_version_affects_output() {
146 let master = Seed::new([3u8; 32]);
147 let id_v1 = ArtifactId::new(
148 "domain:test",
149 "label",
150 b"spec",
151 "variant",
152 DerivationVersion::V1,
153 );
154 let id_v2 = ArtifactId::new(
155 "domain:test",
156 "label",
157 b"spec",
158 "variant",
159 DerivationVersion(2),
160 );
161
162 let v1 = derive_seed(&master, &id_v1);
163 let v2 = derive_seed(&master, &id_v2);
164 assert_ne!(v1.bytes(), v2.bytes());
165 }
166
167 #[test]
168 fn seed_reexport_matches_core_seed() {
169 let seed = Seed::from_env_value("core-id-seed").unwrap();
170 let expected = uselesskey_core_seed::Seed::from_env_value("core-id-seed").unwrap();
171 assert_eq!(seed.bytes(), expected.bytes());
172 }
173
174 #[test]
175 fn derive_seed_label_affects_output() {
176 let master = Seed::new([5u8; 32]);
177 let id_a = ArtifactId::new("d", "label-a", b"spec", "v", DerivationVersion::V1);
178 let id_b = ArtifactId::new("d", "label-b", b"spec", "v", DerivationVersion::V1);
179 assert_ne!(
180 derive_seed(&master, &id_a).bytes(),
181 derive_seed(&master, &id_b).bytes()
182 );
183 }
184
185 #[test]
186 fn derive_seed_domain_affects_output() {
187 let master = Seed::new([6u8; 32]);
188 let id_a = ArtifactId::new("domain:a", "lbl", b"spec", "v", DerivationVersion::V1);
189 let id_b = ArtifactId::new("domain:b", "lbl", b"spec", "v", DerivationVersion::V1);
190 assert_ne!(
191 derive_seed(&master, &id_a).bytes(),
192 derive_seed(&master, &id_b).bytes()
193 );
194 }
195
196 #[test]
197 fn derive_seed_variant_affects_output() {
198 let master = Seed::new([7u8; 32]);
199 let id_a = ArtifactId::new("d", "lbl", b"spec", "good", DerivationVersion::V1);
200 let id_b = ArtifactId::new("d", "lbl", b"spec", "bad", DerivationVersion::V1);
201 assert_ne!(
202 derive_seed(&master, &id_a).bytes(),
203 derive_seed(&master, &id_b).bytes()
204 );
205 }
206
207 #[test]
208 fn derive_seed_spec_affects_output() {
209 let master = Seed::new([8u8; 32]);
210 let id_a = ArtifactId::new("d", "lbl", b"RS256", "v", DerivationVersion::V1);
211 let id_b = ArtifactId::new("d", "lbl", b"RS384", "v", DerivationVersion::V1);
212 assert_ne!(
213 derive_seed(&master, &id_a).bytes(),
214 derive_seed(&master, &id_b).bytes()
215 );
216 }
217
218 #[test]
219 fn derive_seed_master_affects_output() {
220 let id = ArtifactId::new("d", "lbl", b"spec", "v", DerivationVersion::V1);
221 let a = derive_seed(&Seed::new([1u8; 32]), &id);
222 let b = derive_seed(&Seed::new([2u8; 32]), &id);
223 assert_ne!(a.bytes(), b.bytes());
224 }
225
226 #[test]
227 fn artifact_id_empty_fields() {
228 let id = ArtifactId::new("d", "", b"", "", DerivationVersion::V1);
229 assert_eq!(id.label, "");
230 assert_eq!(id.variant, "");
231 assert_eq!(id.spec_fingerprint, *hash32(b"").as_bytes());
232 }
233
234 #[test]
235 fn artifact_id_ordering() {
236 let a = ArtifactId::new("a", "lbl", b"spec", "v", DerivationVersion::V1);
237 let b = ArtifactId::new("b", "lbl", b"spec", "v", DerivationVersion::V1);
238 assert!(a < b, "ArtifactId ordering should be by domain first");
239 }
240
241 #[test]
242 fn artifact_id_clone_equals_original() {
243 let id = ArtifactId::new("d", "lbl", b"spec", "v", DerivationVersion::V1);
244 let cloned = id.clone();
245 assert_eq!(id, cloned);
246 }
247
248 #[test]
249 fn derivation_version_copy_and_hash() {
250 use core::hash::{Hash, Hasher};
251 let v = DerivationVersion::V1;
252 let copy = v;
253 assert_eq!(v, copy);
254
255 let mut h = std::collections::hash_map::DefaultHasher::new();
257 v.hash(&mut h);
258 let hash1 = h.finish();
259
260 let mut h2 = std::collections::hash_map::DefaultHasher::new();
261 copy.hash(&mut h2);
262 assert_eq!(hash1, h2.finish());
263 }
264
265 #[test]
266 fn derivation_version_debug() {
267 let dbg = format!("{:?}", DerivationVersion::V1);
268 assert!(dbg.contains("1"), "Debug should contain the version number");
269 }
270}