1use hkdf::Hkdf;
16use sha2::Sha256;
17
18use super::{ChannelId, ChannelKey, CommunityId, Epoch, Pseudonym, ServerRootKey};
19use nostr_sdk::prelude::SecretKey;
20
21const LABEL_CHANNEL_PSEUDONYM: &str = "vector-community/v1/channel-pseudonym";
24const LABEL_RECIPIENT_PSEUDONYM: &str = "vector-community/v1/recipient-pseudonym";
25const LABEL_REKEY_PSEUDONYM: &str = "vector-community/v1/rekey-pseudonym";
26const LABEL_BASE_REKEY_PSEUDONYM: &str = "vector-community/v1/base-rekey-pseudonym";
27const LABEL_PUBLIC_INVITE_KEY: &str = "vector-community/v1/public-invite-key";
28const LABEL_PUBLIC_INVITE_LOCATOR: &str = "vector-community/v1/public-invite-locator";
29const LABEL_PUBLIC_INVITE_SIGNER: &str = "vector-community/v1/public-invite-signer";
30const LABEL_BANLIST_LOCATOR: &str = "vector-community/v1/banlist-locator";
31const LABEL_GRANT_LOCATOR: &str = "vector-community/v1/grant-locator";
32const LABEL_INVITE_LINKS_LOCATOR: &str = "vector-community/v1/invite-links-locator";
33const LABEL_DISSOLVED_LOCATOR: &str = "vector-community/v1/dissolved-locator";
34const LABEL_DISSOLVED_PSEUDONYM: &str = "vector-community/v1/dissolved-pseudonym";
35const LABEL_DISSOLVED_ENVELOPE: &str = "vector-community/v1/dissolved-envelope-key";
36
37pub fn banlist_locator(community_id: &CommunityId) -> [u8; 32] {
43 hkdf_sha256_32(&community_id.0, LABEL_BANLIST_LOCATOR.as_bytes())
44}
45
46pub fn dissolved_locator(community_id: &CommunityId) -> [u8; 32] {
52 hkdf_sha256_32(&community_id.0, LABEL_DISSOLVED_LOCATOR.as_bytes())
53}
54
55pub fn dissolved_pseudonym(community_id: &CommunityId) -> String {
61 crate::simd::hex::bytes_to_hex_32(&hkdf_sha256_32(&community_id.0, LABEL_DISSOLVED_PSEUDONYM.as_bytes()))
62}
63
64pub fn dissolved_envelope_key(community_id: &CommunityId) -> [u8; 32] {
69 hkdf_sha256_32(&community_id.0, LABEL_DISSOLVED_ENVELOPE.as_bytes())
70}
71
72pub fn invite_links_locator(community_id: &CommunityId, creator_xonly: &[u8; 32]) -> [u8; 32] {
79 let info = build_info(LABEL_INVITE_LINKS_LOCATOR, creator_xonly, None);
80 hkdf_sha256_32(&community_id.0, &info)
81}
82
83pub fn grant_locator(community_id: &CommunityId, member_xonly: &[u8; 32]) -> [u8; 32] {
90 let info = build_info(LABEL_GRANT_LOCATOR, member_xonly, None);
91 hkdf_sha256_32(&community_id.0, &info)
92}
93
94#[derive(Debug, Clone, Copy)]
98pub enum RekeyScope {
99 Channel(ChannelId),
101 ServerRoot,
103}
104
105impl RekeyScope {
106 pub fn id32(&self) -> [u8; 32] {
109 match self {
110 RekeyScope::Channel(c) => c.0,
111 RekeyScope::ServerRoot => [0u8; 32],
112 }
113 }
114}
115
116fn build_info(label: &str, id32: &[u8; 32], epoch: Option<Epoch>) -> Vec<u8> {
119 let mut info = Vec::with_capacity(label.len() + 1 + 32 + 8);
120 info.extend_from_slice(label.as_bytes());
121 info.push(0x00);
122 info.extend_from_slice(id32);
123 if let Some(e) = epoch {
124 info.extend_from_slice(&e.0.to_be_bytes());
125 }
126 info
127}
128
129fn hkdf_sha256_32(ikm: &[u8; 32], info: &[u8]) -> [u8; 32] {
136 let hk = Hkdf::<Sha256>::new(None, ikm);
137 let mut okm = [0u8; 32];
138 hk.expand(info, &mut okm)
139 .expect("HKDF expand of 32 bytes is infallible");
140 okm
141}
142
143pub fn channel_pseudonym(channel_key: &ChannelKey, channel_id: &ChannelId, epoch: Epoch) -> Pseudonym {
147 let info = build_info(LABEL_CHANNEL_PSEUDONYM, &channel_id.0, Some(epoch));
148 Pseudonym(hkdf_sha256_32(channel_key.as_bytes(), &info))
149}
150
151pub fn rekey_pseudonym(server_root: &ServerRootKey, channel_id: &ChannelId, epoch: Epoch) -> Pseudonym {
160 let info = build_info(LABEL_REKEY_PSEUDONYM, &channel_id.0, Some(epoch));
161 Pseudonym(hkdf_sha256_32(server_root.as_bytes(), &info))
162}
163
164pub fn base_rekey_pseudonym(prior_root: &ServerRootKey, community_id: &CommunityId, new_epoch: Epoch) -> Pseudonym {
171 let info = build_info(LABEL_BASE_REKEY_PSEUDONYM, &community_id.0, Some(new_epoch));
172 Pseudonym(hkdf_sha256_32(prior_root.as_bytes(), &info))
173}
174
175pub fn recipient_pseudonym(per_recipient_secret: &[u8; 32], scope: RekeyScope, epoch: Epoch) -> Pseudonym {
179 let info = build_info(LABEL_RECIPIENT_PSEUDONYM, &scope.id32(), Some(epoch));
180 Pseudonym(hkdf_sha256_32(per_recipient_secret, &info))
181}
182
183fn hkdf_to_secret_key(ikm: &[u8; 32], base_info: Vec<u8>) -> SecretKey {
187 let mut counter: u8 = 0;
188 loop {
189 let info = if counter == 0 {
190 base_info.clone()
191 } else {
192 let mut extended = base_info.clone();
193 extended.push(counter);
194 extended
195 };
196 let okm = hkdf_sha256_32(ikm, &info);
197 if let Ok(sk) = SecretKey::from_slice(&okm) {
198 return sk;
199 }
200 counter = counter
201 .checked_add(1)
202 .expect("secp256k1 scalar rejection 256 times running is impossible");
203 }
204}
205
206pub fn public_invite_key(token: &[u8; 32]) -> [u8; 32] {
213 hkdf_sha256_32(token, &build_info(LABEL_PUBLIC_INVITE_KEY, &[0u8; 32], None))
214}
215
216pub fn public_invite_locator(token: &[u8; 32]) -> [u8; 32] {
217 hkdf_sha256_32(token, &build_info(LABEL_PUBLIC_INVITE_LOCATOR, &[0u8; 32], None))
218}
219
220pub fn public_invite_signer(token: &[u8; 32]) -> SecretKey {
221 hkdf_to_secret_key(token, build_info(LABEL_PUBLIC_INVITE_SIGNER, &[0u8; 32], None))
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn test_channel_key() -> ChannelKey {
234 let mut k = [0u8; 32];
236 for (i, b) in k.iter_mut().enumerate() {
237 *b = i as u8;
238 }
239 ChannelKey(k)
240 }
241
242 fn test_channel_id() -> ChannelId {
243 let mut id = [0u8; 32];
245 for (i, b) in id.iter_mut().enumerate() {
246 *b = (255 - i) as u8;
247 }
248 ChannelId(id)
249 }
250
251 #[test]
252 fn channel_pseudonym_is_deterministic() {
253 let key = test_channel_key();
254 let id = test_channel_id();
255 let a = channel_pseudonym(&key, &id, Epoch(0));
256 let b = channel_pseudonym(&key, &id, Epoch(0));
257 assert_eq!(a, b, "same inputs must yield the same pseudonym");
258 }
259
260 #[test]
261 fn channel_pseudonym_golden_epoch0() {
262 let p = channel_pseudonym(&test_channel_key(), &test_channel_id(), Epoch(0));
263 assert_eq!(p.to_hex(), GOLDEN_CHANNEL_PSEUDONYM_EPOCH0);
264 }
265
266 #[test]
267 fn channel_pseudonym_golden_epoch1() {
268 let p = channel_pseudonym(&test_channel_key(), &test_channel_id(), Epoch(1));
269 assert_eq!(p.to_hex(), GOLDEN_CHANNEL_PSEUDONYM_EPOCH1);
270 }
271
272 const GOLDEN_GRANT_LOCATOR: &str =
275 "c18d4d5955ecdd258f44240019a493a01fc01d51b5f0b8f7679ae424f8d5bfcc";
276
277 #[test]
278 fn grant_locator_golden() {
279 let loc = grant_locator(&crate::community::CommunityId([0x11u8; 32]), &[0x22u8; 32]);
280 assert_eq!(crate::simd::hex::bytes_to_hex_32(&loc), GOLDEN_GRANT_LOCATOR);
281 }
282
283 #[test]
284 fn invite_links_locator_golden_and_domain_separated() {
285 let cid = crate::community::CommunityId([0x11u8; 32]);
286 let alice = [0x22u8; 32];
287 let bob = [0x33u8; 32];
288 assert_eq!(
290 crate::simd::hex::bytes_to_hex_32(&invite_links_locator(&cid, &alice)),
291 "cf42937a815ec561da6b4ca5ddd0c361634b0d9744693b744d4f5b34ec209ec2"
292 );
293 assert_ne!(invite_links_locator(&cid, &alice), invite_links_locator(&cid, &bob));
295 assert_ne!(invite_links_locator(&cid, &alice), grant_locator(&cid, &alice));
297 assert_ne!(invite_links_locator(&cid, &alice), banlist_locator(&cid));
298 assert_ne!(invite_links_locator(&cid, &alice), invite_links_locator(&crate::community::CommunityId([0x99u8; 32]), &alice));
300 }
301
302 #[test]
303 fn grant_locator_binds_member_and_community() {
304 let cid = crate::community::CommunityId([0x11u8; 32]);
305 assert_eq!(grant_locator(&cid, &[0x22u8; 32]), grant_locator(&cid, &[0x22u8; 32]));
307 assert_ne!(grant_locator(&cid, &[0x22u8; 32]), grant_locator(&cid, &[0x23u8; 32]));
309 assert_ne!(
312 grant_locator(&cid, &[0x22u8; 32]),
313 grant_locator(&crate::community::CommunityId([0x99u8; 32]), &[0x22u8; 32])
314 );
315 }
316
317 #[test]
318 fn epoch_changes_the_pseudonym() {
319 let key = test_channel_key();
320 let id = test_channel_id();
321 assert_ne!(
322 channel_pseudonym(&key, &id, Epoch(0)),
323 channel_pseudonym(&key, &id, Epoch(1)),
324 "rotating the epoch must rotate the pseudonym (unlinkability)"
325 );
326 }
327
328 #[test]
329 fn different_channel_id_changes_the_pseudonym() {
330 let key = test_channel_key();
331 let other = ChannelId([0x42u8; 32]);
332 assert_ne!(
333 channel_pseudonym(&key, &test_channel_id(), Epoch(0)),
334 channel_pseudonym(&key, &other, Epoch(0)),
335 );
336 }
337
338 #[test]
339 fn different_label_does_not_collide() {
340 let secret = test_channel_key();
343 let id = test_channel_id();
344 let chan = channel_pseudonym(&secret, &id, Epoch(0));
345 let recip = recipient_pseudonym(secret.as_bytes(), RekeyScope::Channel(id), Epoch(0));
346 assert_ne!(chan.0, recip.0, "labels must domain-separate");
347 }
348
349 #[test]
350 fn recipient_pseudonym_golden() {
351 let secret = [7u8; 32];
352 let chan = recipient_pseudonym(&secret, RekeyScope::Channel(test_channel_id()), Epoch(3));
353 let root = recipient_pseudonym(&secret, RekeyScope::ServerRoot, Epoch(3));
354 assert_eq!(chan.to_hex(), GOLDEN_RECIPIENT_CHANNEL_EPOCH3);
355 assert_eq!(root.to_hex(), GOLDEN_RECIPIENT_SERVERROOT_EPOCH3);
356 }
357
358 #[test]
359 fn rekey_pseudonym_is_server_root_derived_and_distinct() {
360 let sr = ServerRootKey([0x07u8; 32]);
361 let chan = test_channel_id();
362 let p = rekey_pseudonym(&sr, &chan, Epoch(1));
364 assert_eq!(p, rekey_pseudonym(&sr, &chan, Epoch(1)));
365 assert_eq!(p.to_hex(), GOLDEN_REKEY_PSEUDONYM);
366 assert_ne!(p, rekey_pseudonym(&ServerRootKey([0x08u8; 32]), &chan, Epoch(1)));
369 assert_ne!(p, rekey_pseudonym(&sr, &chan, Epoch(2)));
371 assert_ne!(p, rekey_pseudonym(&sr, &ChannelId([0x42u8; 32]), Epoch(1)));
372 let as_chan_key = channel_pseudonym(&ChannelKey(*sr.as_bytes()), &chan, Epoch(1));
375 assert_ne!(p.0, as_chan_key.0, "label must domain-separate rekey-address from channel-message");
376 let as_control =
379 crate::community::roster::control_pseudonym(&sr, &crate::community::CommunityId(chan.0), Epoch(1));
380 assert_ne!(p.to_hex(), as_control, "label must domain-separate rekey-address from control-plane");
381 }
382
383 #[test]
384 fn base_rekey_pseudonym_is_prior_root_derived_and_distinct() {
385 let root = ServerRootKey([0x07u8; 32]);
386 let community = crate::community::CommunityId([0x09u8; 32]);
387 let p = base_rekey_pseudonym(&root, &community, Epoch(1));
388 assert_eq!(p, base_rekey_pseudonym(&root, &community, Epoch(1)));
389 assert_eq!(p.to_hex(), GOLDEN_BASE_REKEY_PSEUDONYM);
390 assert_ne!(p, base_rekey_pseudonym(&ServerRootKey([0x08u8; 32]), &community, Epoch(1)));
393 assert_ne!(p, base_rekey_pseudonym(&root, &community, Epoch(2)));
395 assert_ne!(p, base_rekey_pseudonym(&root, &crate::community::CommunityId([0x42u8; 32]), Epoch(1)));
396 let control = super::super::roster::control_pseudonym(&root, &community, Epoch(1));
398 assert_ne!(p.to_hex(), control, "label must domain-separate base-rekey from control-plane");
399 }
400
401 #[test]
402 fn server_root_scope_sentinel_matches_rekey_scope() {
403 assert_eq!(
407 crate::simd::hex::bytes_to_hex_32(&RekeyScope::ServerRoot.id32()),
408 crate::community::SERVER_ROOT_SCOPE_HEX
409 );
410 }
411
412 #[test]
413 fn recipient_scope_disambiguates() {
414 let secret = [7u8; 32];
417 let chan = recipient_pseudonym(&secret, RekeyScope::Channel(test_channel_id()), Epoch(3));
418 let root = recipient_pseudonym(&secret, RekeyScope::ServerRoot, Epoch(3));
419 assert_ne!(chan.0, root.0);
420 }
421
422 #[test]
423 fn channel_pseudonym_golden_multibyte_epoch_is_big_endian() {
424 let p = channel_pseudonym(&test_channel_key(), &test_channel_id(), Epoch(0x0102030405060708));
427 assert_eq!(p.to_hex(), GOLDEN_CHANNEL_PSEUDONYM_EPOCH_BE);
428 }
429
430 #[test]
431 fn public_invite_subkeys_golden() {
432 let token = [5u8; 32];
434 assert_eq!(crate::simd::hex::bytes_to_hex_32(&public_invite_key(&token)), GOLDEN_PUBLIC_INVITE_KEY);
435 assert_eq!(crate::simd::hex::bytes_to_hex_32(&public_invite_locator(&token)), GOLDEN_PUBLIC_INVITE_LOCATOR);
436 assert_eq!(public_invite_signer(&token).to_secret_hex(), GOLDEN_PUBLIC_INVITE_SIGNER);
437 }
438
439 #[test]
440 fn public_invite_subkeys_domain_separated_and_token_bound() {
441 let token = [5u8; 32];
442 let other = [6u8; 32];
443 assert_ne!(public_invite_key(&token), public_invite_locator(&token));
445 assert_ne!(
446 public_invite_key(&token).to_vec(),
447 public_invite_signer(&token).as_secret_bytes().to_vec()
448 );
449 assert_ne!(public_invite_key(&token), public_invite_key(&other));
451 assert_ne!(public_invite_locator(&token), public_invite_locator(&other));
452 }
453
454 const GOLDEN_PUBLIC_INVITE_KEY: &str =
456 "7f02a8a832a1744adf286676038446dc94762c2c8332650c9ad62a0c870e0751";
457 const GOLDEN_PUBLIC_INVITE_LOCATOR: &str =
458 "33c098d6e4cddc2b8ee98ab6b5182186794c35f5b71391130a49ae3d88588c2c";
459 const GOLDEN_PUBLIC_INVITE_SIGNER: &str =
460 "9154a3a7e4a03e94eaad2f76efeebd43e25ee9df4fbca12454edcee0ef666e8d";
461
462 const GOLDEN_REKEY_PSEUDONYM: &str =
464 "3a848655f79a586510e1113131f078aa1ce0ff8dcb74374507e6af07ff49fd24";
465 const GOLDEN_BASE_REKEY_PSEUDONYM: &str =
467 "23ced8fd6cad30a21ded43c96bd040311cf20bcfff935453dc0985b41ff660be";
468
469 const GOLDEN_CHANNEL_PSEUDONYM_EPOCH0: &str =
470 "d55b9f5fad668887d41d46b7c08ba63725a39d7c86b602c7c36e2f2e0eff8c40";
471 const GOLDEN_CHANNEL_PSEUDONYM_EPOCH1: &str =
472 "050079d9899c85bebf5c73fd777cdd812132d262e3ceec83c847a056dea41293";
473 const GOLDEN_RECIPIENT_CHANNEL_EPOCH3: &str =
475 "971f69d6a948c79704f8077188cded86bd35c82960e88043ebb2c2c3d60a3b71";
476 const GOLDEN_RECIPIENT_SERVERROOT_EPOCH3: &str =
477 "e50e5d803fd2edc310be8cd7354586d12fcb8e3f30162553be53da1a34a17c46";
478 const GOLDEN_CHANNEL_PSEUDONYM_EPOCH_BE: &str =
480 "cec398094d17688cd127bc609d34fa067331427400b023d0c70ff77fafe17e0b";
481}