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