1use crate::auth::crypto::{EncryptedData, KdfParams};
14use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
20pub enum FormatError {
21 #[error("Invalid magic bytes")]
22 InvalidMagic,
23 #[error("Unsupported format version: {0}")]
24 UnsupportedVersion(u32),
25 #[error("IO error: {0}")]
26 Io(#[from] std::io::Error),
27 #[error("JSON error: {0}")]
28 Json(#[from] serde_json::Error),
29 #[error("Invalid format: {0}")]
30 InvalidFormat(String),
31}
32
33pub type Result<T> = std::result::Result<T, FormatError>;
34
35const MAGIC: &[u8; 8] = b"SLACKCLI";
36const CURRENT_VERSION: u32 = 1;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ExportProfile {
41 pub team_id: String,
42 pub user_id: String,
43 pub team_name: Option<String>,
44 pub user_name: Option<String>,
45 pub token: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub client_id: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub client_secret: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ExportPayload {
57 pub format_version: u32,
58 pub profiles: HashMap<String, ExportProfile>,
59
60 #[serde(flatten)]
62 #[serde(default)]
63 pub unknown_fields: HashMap<String, serde_json::Value>,
64}
65
66impl ExportPayload {
67 pub fn new() -> Self {
68 Self {
69 format_version: CURRENT_VERSION,
70 profiles: HashMap::new(),
71 unknown_fields: HashMap::new(),
72 }
73 }
74}
75
76impl Default for ExportPayload {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct EncodedExport {
85 pub kdf_params: KdfParams,
86 pub encrypted_data: EncryptedData,
87}
88
89pub fn encode_export(
91 payload: &ExportPayload,
92 encrypted: &EncryptedData,
93 kdf_params: &KdfParams,
94) -> Result<Vec<u8>> {
95 let mut output = Vec::new();
96
97 output.extend_from_slice(MAGIC);
99
100 output.extend_from_slice(&payload.format_version.to_be_bytes());
102
103 let kdf_json = serde_json::json!({
105 "salt": BASE64.encode(&kdf_params.salt),
106 "memory_cost": kdf_params.memory_cost,
107 "time_cost": kdf_params.time_cost,
108 "parallelism": kdf_params.parallelism,
109 });
110 let kdf_bytes = serde_json::to_vec(&kdf_json)?;
111 output.extend_from_slice(&(kdf_bytes.len() as u32).to_be_bytes());
112 output.extend_from_slice(&kdf_bytes);
113
114 output.extend_from_slice(&(encrypted.nonce.len() as u32).to_be_bytes());
116 output.extend_from_slice(&encrypted.nonce);
117
118 output.extend_from_slice(&(encrypted.ciphertext.len() as u32).to_be_bytes());
120 output.extend_from_slice(&encrypted.ciphertext);
121
122 Ok(output)
123}
124
125pub fn decode_export(data: &[u8]) -> Result<EncodedExport> {
127 if data.len() < 8 {
128 return Err(FormatError::InvalidFormat("File too small".to_string()));
129 }
130
131 let mut cursor = 0;
132
133 if &data[cursor..cursor + 8] != MAGIC {
135 return Err(FormatError::InvalidMagic);
136 }
137 cursor += 8;
138
139 if data.len() < cursor + 4 {
141 return Err(FormatError::InvalidFormat(
142 "Missing format version".to_string(),
143 ));
144 }
145 let version = u32::from_be_bytes([
146 data[cursor],
147 data[cursor + 1],
148 data[cursor + 2],
149 data[cursor + 3],
150 ]);
151 cursor += 4;
152
153 if version != CURRENT_VERSION {
154 return Err(FormatError::UnsupportedVersion(version));
155 }
156
157 if data.len() < cursor + 4 {
159 return Err(FormatError::InvalidFormat(
160 "Missing KDF params length".to_string(),
161 ));
162 }
163 let kdf_len = u32::from_be_bytes([
164 data[cursor],
165 data[cursor + 1],
166 data[cursor + 2],
167 data[cursor + 3],
168 ]) as usize;
169 cursor += 4;
170
171 if data.len() < cursor + kdf_len {
172 return Err(FormatError::InvalidFormat(
173 "Missing KDF params data".to_string(),
174 ));
175 }
176 let kdf_json: serde_json::Value = serde_json::from_slice(&data[cursor..cursor + kdf_len])?;
177 cursor += kdf_len;
178
179 let salt = BASE64
180 .decode(
181 kdf_json["salt"]
182 .as_str()
183 .ok_or_else(|| FormatError::InvalidFormat("Missing salt".to_string()))?,
184 )
185 .map_err(|e| FormatError::InvalidFormat(format!("Invalid salt: {}", e)))?;
186
187 let kdf_params = KdfParams {
188 salt,
189 memory_cost: kdf_json["memory_cost"]
190 .as_u64()
191 .ok_or_else(|| FormatError::InvalidFormat("Missing memory_cost".to_string()))?
192 as u32,
193 time_cost: kdf_json["time_cost"]
194 .as_u64()
195 .ok_or_else(|| FormatError::InvalidFormat("Missing time_cost".to_string()))?
196 as u32,
197 parallelism: kdf_json["parallelism"]
198 .as_u64()
199 .ok_or_else(|| FormatError::InvalidFormat("Missing parallelism".to_string()))?
200 as u32,
201 };
202
203 if data.len() < cursor + 4 {
205 return Err(FormatError::InvalidFormat(
206 "Missing nonce length".to_string(),
207 ));
208 }
209 let nonce_len = u32::from_be_bytes([
210 data[cursor],
211 data[cursor + 1],
212 data[cursor + 2],
213 data[cursor + 3],
214 ]) as usize;
215 cursor += 4;
216
217 if data.len() < cursor + nonce_len {
218 return Err(FormatError::InvalidFormat("Missing nonce data".to_string()));
219 }
220 let nonce = data[cursor..cursor + nonce_len].to_vec();
221 cursor += nonce_len;
222
223 if data.len() < cursor + 4 {
225 return Err(FormatError::InvalidFormat(
226 "Missing ciphertext length".to_string(),
227 ));
228 }
229 let ciphertext_len = u32::from_be_bytes([
230 data[cursor],
231 data[cursor + 1],
232 data[cursor + 2],
233 data[cursor + 3],
234 ]) as usize;
235 cursor += 4;
236
237 if data.len() < cursor + ciphertext_len {
238 return Err(FormatError::InvalidFormat(
239 "Missing ciphertext data".to_string(),
240 ));
241 }
242 let ciphertext = data[cursor..cursor + ciphertext_len].to_vec();
243
244 Ok(EncodedExport {
245 kdf_params,
246 encrypted_data: EncryptedData { nonce, ciphertext },
247 })
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::auth::crypto;
254
255 #[test]
256 fn test_export_payload_serialization() {
257 let mut payload = ExportPayload::new();
258 payload.profiles.insert(
259 "default".to_string(),
260 ExportProfile {
261 team_id: "T123".to_string(),
262 user_id: "U456".to_string(),
263 team_name: Some("Test Team".to_string()),
264 user_name: Some("Test User".to_string()),
265 token: "xoxb-test-token".to_string(),
266 client_id: None,
267 client_secret: None,
268 },
269 );
270
271 let json = serde_json::to_string(&payload).unwrap();
272 let deserialized: ExportPayload = serde_json::from_str(&json).unwrap();
273
274 assert_eq!(payload.format_version, deserialized.format_version);
275 assert_eq!(payload.profiles.len(), deserialized.profiles.len());
276 }
277
278 #[test]
279 fn test_export_payload_unknown_fields() {
280 let json = r#"{
281 "format_version": 1,
282 "profiles": {},
283 "future_field": "some_value"
284 }"#;
285
286 let payload: ExportPayload = serde_json::from_str(json).unwrap();
287 assert_eq!(payload.format_version, 1);
288 assert!(payload.unknown_fields.contains_key("future_field"));
289 }
290
291 #[test]
292 fn test_encode_decode_round_trip() {
293 let payload = ExportPayload::new();
294 let passphrase = "test_password";
295
296 let kdf_params = KdfParams {
298 salt: crypto::generate_salt(),
299 ..Default::default()
300 };
301
302 let payload_json = serde_json::to_vec(&payload).unwrap();
304 let key = crypto::derive_key(passphrase, &kdf_params).unwrap();
305 let encrypted = crypto::encrypt(&payload_json, &key).unwrap();
306
307 let encoded = encode_export(&payload, &encrypted, &kdf_params).unwrap();
309
310 let decoded = decode_export(&encoded).unwrap();
312
313 assert_eq!(kdf_params.salt, decoded.kdf_params.salt);
315 assert_eq!(kdf_params.memory_cost, decoded.kdf_params.memory_cost);
316 assert_eq!(kdf_params.time_cost, decoded.kdf_params.time_cost);
317 assert_eq!(kdf_params.parallelism, decoded.kdf_params.parallelism);
318
319 assert_eq!(encrypted.nonce, decoded.encrypted_data.nonce);
321 assert_eq!(encrypted.ciphertext, decoded.encrypted_data.ciphertext);
322 }
323
324 #[test]
325 fn test_decode_invalid_magic() {
326 let data = b"INVALID_DATA";
327 let result = decode_export(data);
328 assert!(result.is_err());
329 assert!(matches!(result.unwrap_err(), FormatError::InvalidMagic));
330 }
331
332 #[test]
333 fn test_decode_unsupported_version() {
334 let mut data = Vec::new();
335 data.extend_from_slice(MAGIC);
336 data.extend_from_slice(&999u32.to_be_bytes()); let result = decode_export(&data);
339 assert!(result.is_err());
340 assert!(matches!(
341 result.unwrap_err(),
342 FormatError::UnsupportedVersion(999)
343 ));
344 }
345}