Skip to main content

citadel_sync/
sync_key.rs

1use base64::Engine;
2use zeroize::Zeroize;
3
4const KEY_SIZE: usize = 32;
5
6/// 32-byte pre-shared key for encrypted sync transport.
7#[derive(Clone)]
8pub struct SyncKey([u8; KEY_SIZE]);
9
10impl SyncKey {
11    /// Generate a random sync key.
12    pub fn generate() -> Self {
13        use rand::RngCore;
14        let mut key = [0u8; KEY_SIZE];
15        rand::thread_rng().fill_bytes(&mut key);
16        Self(key)
17    }
18
19    /// Create from raw bytes.
20    pub fn from_bytes(bytes: [u8; KEY_SIZE]) -> Self {
21        Self(bytes)
22    }
23
24    /// Decode from base64.
25    pub fn from_base64(s: &str) -> Result<Self, SyncKeyError> {
26        let bytes = base64::engine::general_purpose::STANDARD
27            .decode(s)
28            .map_err(|e| SyncKeyError(e.to_string()))?;
29        if bytes.len() != KEY_SIZE {
30            return Err(SyncKeyError(format!(
31                "expected {} bytes, got {}",
32                KEY_SIZE,
33                bytes.len()
34            )));
35        }
36        let mut arr = [0u8; KEY_SIZE];
37        arr.copy_from_slice(&bytes);
38        Ok(Self(arr))
39    }
40
41    /// Encode as base64.
42    pub fn to_base64(&self) -> String {
43        base64::engine::general_purpose::STANDARD.encode(self.0)
44    }
45
46    /// Borrow the raw bytes.
47    pub fn as_bytes(&self) -> &[u8; KEY_SIZE] {
48        &self.0
49    }
50}
51
52impl Drop for SyncKey {
53    fn drop(&mut self) {
54        self.0.zeroize();
55    }
56}
57
58impl std::fmt::Debug for SyncKey {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str("SyncKey([REDACTED])")
61    }
62}
63
64impl std::fmt::Display for SyncKey {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.write_str(&self.to_base64())
67    }
68}
69
70/// Error decoding a sync key.
71#[derive(Debug, Clone)]
72pub struct SyncKeyError(pub String);
73
74impl std::fmt::Display for SyncKeyError {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "invalid sync key: {}", self.0)
77    }
78}
79
80impl std::error::Error for SyncKeyError {}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn generate_unique() {
88        let a = SyncKey::generate();
89        let b = SyncKey::generate();
90        assert_ne!(a.0, b.0);
91    }
92
93    #[test]
94    fn base64_roundtrip() {
95        let key = SyncKey::generate();
96        let encoded = key.to_base64();
97        let decoded = SyncKey::from_base64(&encoded).unwrap();
98        assert_eq!(key.0, decoded.0);
99    }
100
101    #[test]
102    fn from_bytes_roundtrip() {
103        let raw = [0xABu8; KEY_SIZE];
104        let key = SyncKey::from_bytes(raw);
105        assert_eq!(*key.as_bytes(), raw);
106    }
107
108    #[test]
109    fn invalid_base64_rejected() {
110        assert!(SyncKey::from_base64("not-valid-base64!!!").is_err());
111    }
112
113    #[test]
114    fn wrong_length_rejected() {
115        let short = base64::engine::general_purpose::STANDARD.encode([0u8; 16]);
116        assert!(SyncKey::from_base64(&short).is_err());
117    }
118
119    #[test]
120    fn debug_redacts() {
121        let key = SyncKey::generate();
122        let debug = format!("{:?}", key);
123        assert_eq!(debug, "SyncKey([REDACTED])");
124        assert!(!debug.contains(&key.to_base64()));
125    }
126
127    #[test]
128    fn display_is_base64() {
129        let key = SyncKey::generate();
130        assert_eq!(format!("{}", key), key.to_base64());
131    }
132
133    #[test]
134    fn base64_length_is_44() {
135        let key = SyncKey::generate();
136        assert_eq!(key.to_base64().len(), 44);
137    }
138}