distributed_topic_tracker/crypto/
keys.rs

1use std::sync::Arc;
2
3use sha2::Digest;
4
5use crate::crypto::RecordTopic;
6
7/// Trait for deriving time-rotated encryption keys.
8///
9/// Implementations control how encryption keys rotate based on time,
10/// providing key isolation across time slots.
11pub trait SecretRotation: Send + Sync {
12    /// Derive an encryption key for a specific time slot.
13    ///
14    /// # Arguments
15    ///
16    /// * `topic_hash` - 32-byte topic identifier
17    /// * `unix_minute` - Time slot (minute precision)
18    /// * `initial_secret_hash` - 32-byte hashed initial secret
19    ///
20    /// # Returns
21    ///
22    /// A 32-byte derived key unique to this topic/time combination.
23    fn derive(
24        &self,
25        topic_hash: [u8; 32],
26        unix_minute: u64,
27        initial_secret_hash: [u8; 32],
28    ) -> [u8; 32];
29}
30
31/// Default implementation: SHA512-based KDF.
32///
33/// Combines topic hash, time slot, and initial secret into a unique key.
34#[derive(Debug, Clone)]
35pub struct DefaultSecretRotation;
36
37impl SecretRotation for DefaultSecretRotation {
38    fn derive(
39        &self,
40        topic_hash: [u8; 32],
41        unix_minute: u64,
42        initial_secret_hash: [u8; 32],
43    ) -> [u8; 32] {
44        use sha2::Digest;
45        let mut h = sha2::Sha512::new();
46        h.update(topic_hash);
47        h.update(unix_minute.to_be_bytes());
48        h.update(initial_secret_hash);
49        h.finalize()[..32].try_into().unwrap()
50    }
51}
52
53/// Wrapper for custom or default secret rotation implementations.
54///
55/// Allows pluggable key derivation strategies while maintaining a consistent API.
56#[derive(Clone)]
57pub struct RotationHandle(Arc<dyn SecretRotation>);
58
59impl core::fmt::Debug for RotationHandle {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("RotationHandle").finish()
62    }
63}
64
65impl Default for RotationHandle {
66    fn default() -> Self {
67        Self(Arc::new(DefaultSecretRotation))
68    }
69}
70
71impl RotationHandle {
72    /// Create a new rotation handle with a custom implementation.
73    pub fn new(rotation: impl SecretRotation + 'static) -> Self {
74        Self(Arc::new(rotation))
75    }
76
77    /// Derive a key using the underlying strategy.
78    pub fn derive(
79        &self,
80        topic_hash: [u8; 32],
81        unix_minute: u64,
82        initial_secret_hash: [u8; 32],
83    ) -> [u8; 32] {
84        self.0.derive(topic_hash, unix_minute, initial_secret_hash)
85    }
86}
87
88/// Derive Ed25519 signing key for DHT record authentication.
89///
90/// Keys are deterministic per topic and time slot, derived from topic hash.
91/// All nodes use the same derived keypair for a given topic+time combination,
92/// and its verifying key serves as the DHT routing key for storing/retrieving
93/// bootstrap records. The actual record content is signed separately by each
94/// node's individual keypair (not this one).
95///
96/// # Example
97///
98/// ```ignore
99/// let topic = RecordTopic::from_str("my-topic")?;
100/// let unix_minute = crate::unix_minute(0);
101/// let signing_key = signing_keypair(topic, unix_minute);
102/// ```
103pub fn signing_keypair(record_topic: RecordTopic, unix_minute: u64) -> ed25519_dalek::SigningKey {
104    let mut sign_keypair_hash = sha2::Sha512::new();
105    sign_keypair_hash.update(record_topic.hash());
106    sign_keypair_hash.update(unix_minute.to_le_bytes());
107    let sign_keypair_seed: [u8; 32] = sign_keypair_hash.finalize()[..32]
108        .try_into()
109        .expect("hashing failed");
110    ed25519_dalek::SigningKey::from_bytes(&sign_keypair_seed)
111}
112
113/// Derive Ed25519 key for HPKE encryption/decryption.
114///
115/// Incorporates the secret rotation strategy for time-slot isolation.
116///
117/// # Example
118///
119/// ```ignore
120/// let topic = RecordTopic::from_str("my-topic")?;
121/// let rotation = RotationHandle::default();
122/// let enc_key = encryption_keypair(topic, &rotation, initial_hash, 0);
123/// ```
124pub fn encryption_keypair(
125    record_topic: RecordTopic,
126    secret_rotation_function: &RotationHandle,
127    initial_secret_hash: [u8; 32],
128    unix_minute: u64,
129) -> ed25519_dalek::SigningKey {
130    let enc_keypair_seed =
131        secret_rotation_function
132            .0
133            .derive(record_topic.hash(), unix_minute, initial_secret_hash);
134    ed25519_dalek::SigningKey::from_bytes(&enc_keypair_seed)
135}
136
137/// Derive DHT salt for mutable record lookups.
138///
139/// Salt = SHA512(topic_hash || unix_minute.to_le_bytes())[..32]
140/// Ensures records are stored in different DHT slots per minute.
141pub fn salt(record_topic: RecordTopic, unix_minute: u64) -> [u8; 32] {
142    let mut slot_hash = sha2::Sha512::new();
143    slot_hash.update(record_topic.hash());
144    slot_hash.update(unix_minute.to_le_bytes());
145    slot_hash.finalize()[..32]
146        .try_into()
147        .expect("hashing failed")
148}