1use base64::Engine;
2use zeroize::Zeroize;
3
4const KEY_SIZE: usize = 32;
5
6#[derive(Clone)]
8pub struct SyncKey([u8; KEY_SIZE]);
9
10impl SyncKey {
11 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 pub fn from_bytes(bytes: [u8; KEY_SIZE]) -> Self {
21 Self(bytes)
22 }
23
24 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 pub fn to_base64(&self) -> String {
43 base64::engine::general_purpose::STANDARD.encode(self.0)
44 }
45
46 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#[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}