1use hkdf::Hkdf;
26use nostr_sdk::prelude::nip44::v2::ConversationKey;
27use nostr_sdk::prelude::{Keys, PublicKey, SecretKey};
28use sha2::{Digest, Sha256};
29
30use super::super::{ChannelId, CommunityId, Epoch};
31
32const LABEL_CHANNEL: &str = "concord/channel";
34const LABEL_CONTROL: &str = "concord/control";
35const LABEL_CONTROL_SIGNER: &str = "concord/control-signer";
36const LABEL_REKEY_PSEUDONYM: &str = "concord/rekey-pseudonym";
37const LABEL_BASE_REKEY_PSEUDONYM: &str = "concord/base-rekey-pseudonym";
38const LABEL_RECIPIENT_PSEUDONYM: &str = "concord/recipient-pseudonym";
39const LABEL_GUESTBOOK: &str = "concord/guestbook";
40const LABEL_VOICE_SIGNER: &str = "concord/voice-signer";
41const LABEL_VOICE_MEDIA: &str = "concord/voice-media";
42const LABEL_VOICE_SENDER: &str = "concord/voice-sender";
43const LABEL_DISSOLVED: &str = "concord/dissolved";
44const LABEL_GRANT: &str = "concord/grant";
45const LABEL_BANLIST: &str = "concord/banlist";
46const LABEL_PINS: &str = "concord/pins";
47const LABEL_INVITE_LINKS: &str = "concord/invite-links";
48const LABEL_INVITE_KEY: &str = "concord/invite-key";
49const LABEL_COMMUNITY: &str = "concord/community";
51const LABEL_EPOCH_COMMITMENT: &str = "concord/epoch-key-commitment";
53
54const ZERO32: [u8; 32] = [0u8; 32];
55
56pub const TOKEN_LEN: usize = 16;
59
60fn build_info(label: &str, id32: &[u8; 32], epoch: Option<u64>) -> Vec<u8> {
63 let mut info = Vec::with_capacity(label.len() + 1 + 32 + 8);
64 info.extend_from_slice(label.as_bytes());
65 info.push(0x00);
66 info.extend_from_slice(id32);
67 if let Some(e) = epoch {
68 info.extend_from_slice(&e.to_be_bytes());
69 }
70 info
71}
72
73fn hkdf32(ikm: &[u8], info: &[u8]) -> [u8; 32] {
77 let hk = Hkdf::<Sha256>::new(None, ikm);
78 let mut okm = [0u8; 32];
79 hk.expand(info, &mut okm)
80 .expect("HKDF expand of 32 bytes is infallible");
81 okm
82}
83
84fn hkdf_to_secret_key(ikm: &[u8], base_info: &[u8]) -> SecretKey {
89 if let Ok(sk) = SecretKey::from_slice(&hkdf32(ikm, base_info)) {
90 return sk;
91 }
92 for counter in 0u8..=255 {
93 let mut info = base_info.to_vec();
94 info.push(counter);
95 if let Ok(sk) = SecretKey::from_slice(&hkdf32(ikm, &info)) {
96 return sk;
97 }
98 }
99 unreachable!("secp256k1 scalar rejection 257 times running is impossible")
100}
101
102#[derive(Clone)]
108pub struct GroupKey {
109 keys: Keys,
110 conv_key: ConversationKey,
111}
112
113impl GroupKey {
114 pub(crate) fn from_parts(keys: Keys, conv_key: ConversationKey) -> Self {
119 GroupKey { keys, conv_key }
120 }
121
122 fn derive(label: &str, secret: &[u8], id32: &[u8; 32], epoch: Option<u64>) -> Self {
123 let memo_key: [u8; 32] = {
130 let mut h = Sha256::new();
131 h.update(label.as_bytes());
132 h.update([0u8]);
133 h.update(secret);
134 h.update(id32);
135 h.update(epoch.unwrap_or(u64::MAX).to_be_bytes());
136 h.update([epoch.is_some() as u8]);
137 h.finalize().into()
138 };
139 struct GroupKeyMemo;
140 type MemoMap = std::sync::Mutex<std::collections::HashMap<[u8; 32], GroupKey>>;
141 let memo: std::sync::Arc<MemoMap> =
142 crate::db::current_session().scoped::<GroupKeyMemo, MemoMap>();
143 if let Some(hit) = memo.lock().unwrap_or_else(|e| e.into_inner()).get(&memo_key) {
144 return hit.clone();
145 }
146 let info = build_info(label, id32, epoch);
147 let sk = hkdf_to_secret_key(secret, &info);
148 let keys = Keys::new(sk);
149 let conv_key = ConversationKey::derive(keys.secret_key(), &keys.public_key())
150 .expect("self-ECDH of a valid keypair cannot fail");
151 let out = GroupKey { keys, conv_key };
152 let mut map = memo.lock().unwrap_or_else(|e| e.into_inner());
153 if map.len() >= 1024 {
156 map.clear();
157 }
158 map.insert(memo_key, out.clone());
159 out
160 }
161
162 pub fn pk(&self) -> PublicKey {
164 self.keys.public_key()
165 }
166
167 pub fn pk_hex(&self) -> String {
169 self.keys.public_key().to_hex()
170 }
171
172 pub fn keys(&self) -> &Keys {
174 &self.keys
175 }
176
177 pub fn conv_key(&self) -> &ConversationKey {
179 &self.conv_key
180 }
181}
182
183impl std::fmt::Debug for GroupKey {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 f.debug_struct("GroupKey").field("pk", &self.pk_hex()).finish()
187 }
188}
189
190pub fn channel_group_key(secret: &[u8; 32], channel_id: &ChannelId, epoch: Epoch) -> GroupKey {
198 GroupKey::derive(LABEL_CHANNEL, secret, &channel_id.0, Some(epoch.0))
199}
200
201pub fn control_group_key(community_root: &[u8; 32], community_id: &CommunityId, epoch: Epoch) -> GroupKey {
208 GroupKey::derive(LABEL_CONTROL, community_root, &community_id.0, Some(epoch.0))
209}
210
211pub fn control_signer_group_key(control_root: &[u8; 32], community_id: &CommunityId, epoch: Epoch) -> GroupKey {
217 GroupKey::derive(LABEL_CONTROL_SIGNER, control_root, &community_id.0, Some(epoch.0))
218}
219
220pub fn guestbook_group_key(community_root: &[u8; 32], community_id: &CommunityId, epoch: Epoch) -> GroupKey {
222 GroupKey::derive(LABEL_GUESTBOOK, community_root, &community_id.0, Some(epoch.0))
223}
224
225pub fn channel_rekey_group_key(root: &[u8; 32], channel_id: &ChannelId, new_epoch: Epoch) -> GroupKey {
230 GroupKey::derive(LABEL_REKEY_PSEUDONYM, root, &channel_id.0, Some(new_epoch.0))
231}
232
233pub fn base_rekey_group_key(prior_root: &[u8; 32], community_id: &CommunityId, new_epoch: Epoch) -> GroupKey {
237 GroupKey::derive(LABEL_BASE_REKEY_PSEUDONYM, prior_root, &community_id.0, Some(new_epoch.0))
238}
239
240pub fn dissolved_group_key(community_id: &CommunityId) -> GroupKey {
244 GroupKey::derive(LABEL_DISSOLVED, &community_id.0, &ZERO32, None)
245}
246
247pub fn voice_group_key(secret: &[u8; 32], channel_id: &ChannelId, epoch: Epoch) -> GroupKey {
254 GroupKey::derive(LABEL_VOICE_SIGNER, secret, &channel_id.0, Some(epoch.0))
255}
256
257pub fn voice_media_key(secret: &[u8; 32], channel_id: &ChannelId, epoch: Epoch) -> [u8; 32] {
260 hkdf32(secret, &build_info(LABEL_VOICE_MEDIA, &channel_id.0, Some(epoch.0)))
261}
262
263pub fn voice_sender_key(media_key: &[u8; 32], identity: &str) -> [u8; 32] {
268 let id: [u8; 32] = Sha256::digest(identity.as_bytes()).into();
269 hkdf32(media_key, &build_info(LABEL_VOICE_SENDER, &id, None))
270}
271
272pub fn grant_locator(community_id: &CommunityId, member_xonly: &[u8; 32]) -> [u8; 32] {
277 hkdf32(&community_id.0, &build_info(LABEL_GRANT, member_xonly, None))
278}
279
280pub fn banlist_locator(community_id: &CommunityId) -> [u8; 32] {
282 hkdf32(&community_id.0, &build_info(LABEL_BANLIST, &ZERO32, None))
283}
284
285pub fn pins_locator(community_id: &CommunityId, channel_id: &ChannelId) -> [u8; 32] {
287 hkdf32(&community_id.0, &build_info(LABEL_PINS, &channel_id.0, None))
288}
289
290pub fn invite_links_locator(community_id: &CommunityId, creator_xonly: &[u8; 32]) -> [u8; 32] {
293 hkdf32(&community_id.0, &build_info(LABEL_INVITE_LINKS, creator_xonly, None))
294}
295
296pub fn recipient_locator(
305 rotator_xonly: &[u8; 32],
306 recipient_xonly: &[u8; 32],
307 scope_id: &[u8; 32],
308 new_epoch: Epoch,
309) -> [u8; 32] {
310 let mut ikm = [0u8; 64];
311 ikm[..32].copy_from_slice(rotator_xonly);
312 ikm[32..].copy_from_slice(recipient_xonly);
313 hkdf32(&ikm, &build_info(LABEL_RECIPIENT_PSEUDONYM, scope_id, Some(new_epoch.0)))
314}
315
316pub fn invite_bundle_key(token: &[u8; TOKEN_LEN]) -> [u8; 32] {
319 hkdf32(token, &build_info(LABEL_INVITE_KEY, &ZERO32, None))
320}
321
322pub fn community_id_of(owner_xonly: &[u8; 32], owner_salt: &[u8; 32]) -> CommunityId {
330 let mut h = Sha256::new();
331 h.update(LABEL_COMMUNITY.as_bytes());
332 h.update(owner_xonly);
333 h.update(owner_salt);
334 CommunityId(h.finalize().into())
335}
336
337pub fn verify_community_id(community_id: &CommunityId, owner_xonly: &[u8; 32], owner_salt: &[u8; 32]) -> bool {
341 community_id_of(owner_xonly, owner_salt) == *community_id
342}
343
344pub fn epoch_key_commitment(prev_epoch: Epoch, prev_key: &[u8; 32]) -> [u8; 32] {
350 let mut h = Sha256::new();
351 h.update(LABEL_EPOCH_COMMITMENT.as_bytes());
352 h.update(prev_epoch.0.to_be_bytes());
353 h.update(prev_key);
354 h.finalize().into()
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn secret() -> [u8; 32] {
369 let mut k = [0u8; 32];
371 for (i, b) in k.iter_mut().enumerate() {
372 *b = i as u8;
373 }
374 k
375 }
376
377 fn id32() -> [u8; 32] {
378 let mut id = [0u8; 32];
380 for (i, b) in id.iter_mut().enumerate() {
381 *b = (255 - i) as u8;
382 }
383 id
384 }
385
386 fn alt() -> [u8; 32] {
387 [0x11u8; 32]
388 }
389
390 fn cid() -> CommunityId {
391 CommunityId(id32())
392 }
393
394 fn chan() -> ChannelId {
395 ChannelId(id32())
396 }
397
398 const EPOCH_MULTI: u64 = 0x0102030405060708;
400
401 const GOLDEN_CHANNEL_E0_SEED: &str = "1a99a5958bf9fcc5336e6e19db42aabf36ffbfa12f38a1d5fbde2ae383ed751b";
402 const GOLDEN_CHANNEL_E0_PK: &str = "7a5c5dff759a63f1fc2779864487432bae3d1ea72c4ffabd39f4c1fdaf62097a";
403 const GOLDEN_CHANNEL_EMULTI_PK: &str = "f20c7d192cc87615d7341e86f38f85303f4708b40232d4fea521ab8217767391";
404 const GOLDEN_CONTROL_E0_PK: &str = "c43df20bf4d6eeaea5149619662ffe9b211f31e11bb4a59f56b6e906f702d46f";
405 const GOLDEN_CONTROL_SIGNER_E0_SEED: &str = "c4a3e8354d95137132087356412b67b53e025d127d45de45cff9ecf45b0c24f6";
406 const GOLDEN_CONTROL_SIGNER_E0_PK: &str = "718aef388257f3fd9f1bfae5cf2cbd0594a2ffc31adb5c1fe22c502c046acaee";
407 const GOLDEN_CONTROL_SIGNER_EMULTI_PK: &str = "e27235cc13be2f9ad65648e01ff2b63402846469c8638b5386c625688194ec7d";
408 const GOLDEN_GUESTBOOK_E0_PK: &str = "ad09de582026fa7a052db18bb5827fa24c15e929d59aadcc91efb8508f5368ad";
409 const GOLDEN_CHANNEL_REKEY_E1_PK: &str = "7c55cdb957e9db2b4800d687b2a07d3f7066b1a35824a1e86ba871f55e87e8b5";
410 const GOLDEN_BASE_REKEY_E1_PK: &str = "fb2fa44fba66ba15595f784255a1cb569531db8784432ac0e4fe838498dd9dea";
411 const GOLDEN_DISSOLVED_PK: &str = "4d3d55d88fdf9d9c2089651e5cbb0dfa93b6b9b10cdcb2319b0dce1a1398096a";
412 const GOLDEN_GRANT_LOCATOR: &str = "fd2f88cc7f1eb8d7d862c91dc22afe700c358d1845158b3f353b769ce4898e35";
413 const GOLDEN_BANLIST_LOCATOR: &str = "88089214afae6d3c412fd817ada44d6df4d485a53565646471e74476397693c9";
414 const GOLDEN_INVITE_LINKS_LOCATOR: &str = "f4ae29994165767bac23e8dce630f81b926d2c8aa150e5cbf0bdf75865e8379a";
415 const GOLDEN_RECIPIENT_LOCATOR: &str = "342deb400e191f0f52c81f27600934552550beb85aa9bf169f02d0e7f826cf74";
416 const GOLDEN_INVITE_KEY: &str = "94bf8b0d89e579ddaeccf8d9db3f5de5c86a1259c597f2560ff0120173bc5e1f";
417 const GOLDEN_VOICE_MEDIA_E0: &str = "8ab5b935c5e17f156563860ae6263f3700bfd836c326f8b3d7082be2fbaef6a0";
418 const GOLDEN_VOICE_SIGNER_E0_PK: &str = "7591f1306c265ee1dce6a07b72c76fadb3af13bf9ccce0d284cb3af6134211a1";
419 const GOLDEN_VOICE_SENDER: &str = "9ce1c11a39ce16a84b72c2697724a39e5c41ec07f4d703dcb01241271837599e";
420 const GOLDEN_COMMUNITY_ID: &str = "2b790bd59df98bdc52092b74ebd6933a89ef8eaeecc9030861cbdeae7c814c46";
421 const GOLDEN_EPOCH_COMMITMENT: &str = "3e6d6a3c9973c16d1ca7c5602d36979927c55c21a7e2c840f883af3f047e80a4";
422
423 fn hex(bytes: &[u8]) -> String {
424 crate::simd::hex::bytes_to_hex_32(bytes.try_into().expect("32 bytes"))
425 }
426
427 #[test]
428 fn channel_group_key_golden_vector() {
429 let gk = channel_group_key(&secret(), &chan(), Epoch(0));
430 assert_eq!(hex(gk.keys().secret_key().as_secret_bytes()), GOLDEN_CHANNEL_E0_SEED);
433 assert_eq!(gk.pk_hex(), GOLDEN_CHANNEL_E0_PK);
434 }
435
436 #[test]
437 fn channel_group_key_golden_multibyte_epoch_is_big_endian() {
438 let gk = channel_group_key(&secret(), &chan(), Epoch(EPOCH_MULTI));
439 assert_eq!(gk.pk_hex(), GOLDEN_CHANNEL_EMULTI_PK);
440 }
441
442 #[test]
443 fn control_group_key_golden_vector() {
444 assert_eq!(control_group_key(&secret(), &cid(), Epoch(0)).pk_hex(), GOLDEN_CONTROL_E0_PK);
445 }
446
447 #[test]
448 fn control_signer_group_key_golden_vectors() {
449 let gk = control_signer_group_key(&secret(), &cid(), Epoch(0));
452 assert_eq!(hex(gk.keys().secret_key().as_secret_bytes()), GOLDEN_CONTROL_SIGNER_E0_SEED);
453 assert_eq!(gk.pk_hex(), GOLDEN_CONTROL_SIGNER_E0_PK);
454 assert_ne!(gk.pk_hex(), GOLDEN_CONTROL_E0_PK);
455 assert_eq!(
456 control_signer_group_key(&secret(), &cid(), Epoch(EPOCH_MULTI)).pk_hex(),
457 GOLDEN_CONTROL_SIGNER_EMULTI_PK
458 );
459 }
460
461 #[test]
462 fn guestbook_group_key_golden_vector() {
463 assert_eq!(guestbook_group_key(&secret(), &cid(), Epoch(0)).pk_hex(), GOLDEN_GUESTBOOK_E0_PK);
464 }
465
466 #[test]
467 fn rekey_group_keys_golden_vectors() {
468 assert_eq!(
469 channel_rekey_group_key(&secret(), &chan(), Epoch(1)).pk_hex(),
470 GOLDEN_CHANNEL_REKEY_E1_PK
471 );
472 assert_eq!(
473 base_rekey_group_key(&secret(), &cid(), Epoch(1)).pk_hex(),
474 GOLDEN_BASE_REKEY_E1_PK
475 );
476 }
477
478 #[test]
479 fn dissolved_group_key_golden_and_is_epoch_free() {
480 assert_eq!(dissolved_group_key(&cid()).pk_hex(), GOLDEN_DISSOLVED_PK);
481 let with_epoch = GroupKey::derive(LABEL_DISSOLVED, &cid().0, &ZERO32, Some(0));
484 assert_ne!(with_epoch.pk_hex(), GOLDEN_DISSOLVED_PK);
485 }
486
487 #[test]
488 fn locator_golden_vectors() {
489 assert_eq!(hex(&grant_locator(&cid(), &alt())), GOLDEN_GRANT_LOCATOR);
490 assert_eq!(hex(&banlist_locator(&cid())), GOLDEN_BANLIST_LOCATOR);
491 assert_eq!(hex(&invite_links_locator(&cid(), &alt())), GOLDEN_INVITE_LINKS_LOCATOR);
492 assert_eq!(
493 hex(&recipient_locator(&secret(), &alt(), &id32(), Epoch(3))),
494 GOLDEN_RECIPIENT_LOCATOR
495 );
496 assert_eq!(hex(&invite_bundle_key(&[0x07u8; TOKEN_LEN])), GOLDEN_INVITE_KEY);
497 }
498
499 #[test]
500 fn voice_golden_vectors() {
501 let media = voice_media_key(&secret(), &chan(), Epoch(0));
502 assert_eq!(hex(&media), GOLDEN_VOICE_MEDIA_E0);
503 assert_eq!(voice_group_key(&secret(), &chan(), Epoch(0)).pk_hex(), GOLDEN_VOICE_SIGNER_E0_PK);
504 assert_eq!(
505 hex(&voice_sender_key(&media, "00112233445566778899aabbccddeeff")),
506 GOLDEN_VOICE_SENDER
507 );
508 }
509
510 #[test]
511 fn community_id_golden_and_verifies() {
512 let id = community_id_of(&secret(), &alt());
513 assert_eq!(hex(&id.0), GOLDEN_COMMUNITY_ID);
514 assert!(verify_community_id(&id, &secret(), &alt()));
515 assert!(!verify_community_id(&id, &alt(), &alt()));
517 assert!(!verify_community_id(&id, &secret(), &id32()));
518 }
519
520 #[test]
521 fn epoch_key_commitment_golden_and_binds_both_inputs() {
522 assert_eq!(hex(&epoch_key_commitment(Epoch(2), &secret())), GOLDEN_EPOCH_COMMITMENT);
523 assert_ne!(hex(&epoch_key_commitment(Epoch(3), &secret())), GOLDEN_EPOCH_COMMITMENT);
524 assert_ne!(hex(&epoch_key_commitment(Epoch(2), &alt())), GOLDEN_EPOCH_COMMITMENT);
525 }
526
527 #[test]
528 fn labels_domain_separate_every_plane() {
529 let pks = [
532 channel_group_key(&secret(), &chan(), Epoch(0)).pk_hex(),
533 control_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
534 control_signer_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
535 guestbook_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
536 channel_rekey_group_key(&secret(), &chan(), Epoch(0)).pk_hex(),
537 base_rekey_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
538 voice_group_key(&secret(), &chan(), Epoch(0)).pk_hex(),
539 ];
540 let unique: std::collections::HashSet<_> = pks.iter().collect();
541 assert_eq!(unique.len(), pks.len(), "two labels collided on one address");
542 }
543
544 #[test]
545 fn epoch_rotates_every_keyed_address() {
546 assert_ne!(
547 channel_group_key(&secret(), &chan(), Epoch(0)).pk_hex(),
548 channel_group_key(&secret(), &chan(), Epoch(1)).pk_hex()
549 );
550 assert_ne!(
551 control_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
552 control_group_key(&secret(), &cid(), Epoch(1)).pk_hex()
553 );
554 assert_ne!(
555 guestbook_group_key(&secret(), &cid(), Epoch(0)).pk_hex(),
556 guestbook_group_key(&secret(), &cid(), Epoch(1)).pk_hex()
557 );
558 }
559
560 #[test]
561 fn recipient_locator_binds_direction_scope_and_epoch() {
562 let base = recipient_locator(&secret(), &alt(), &id32(), Epoch(1));
563 assert_ne!(recipient_locator(&alt(), &secret(), &id32(), Epoch(1)), base);
565 assert_ne!(recipient_locator(&secret(), &alt(), &id32(), Epoch(2)), base);
566 assert_ne!(recipient_locator(&secret(), &alt(), &ZERO32, Epoch(1)), base);
567 }
568
569 #[test]
570 fn conv_key_is_deterministic_self_ecdh() {
571 let a = channel_group_key(&secret(), &chan(), Epoch(0));
572 let b = channel_group_key(&secret(), &chan(), Epoch(0));
573 assert_eq!(a.conv_key().as_bytes(), b.conv_key().as_bytes());
574 }
575}