Skip to main content

slack_rs/auth/
format.rs

1//! Export/import file format definition
2//!
3//! Binary format:
4//! - Magic bytes (8 bytes): "SLACKCLI"
5//! - Format version (4 bytes, u32, big-endian)
6//! - KDF params length (4 bytes, u32, big-endian)
7//! - KDF params (variable length, JSON)
8//! - Nonce length (4 bytes, u32, big-endian)
9//! - Nonce (variable length)
10//! - Ciphertext length (4 bytes, u32, big-endian)
11//! - Ciphertext (variable length)
12
13use 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/// Profile data for export (includes token and optional OAuth credentials)
39#[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    /// OAuth client ID (optional for backward compatibility)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub client_id: Option<String>,
49    /// OAuth client secret (optional for backward compatibility)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub client_secret: Option<String>,
52    /// User token (optional for backward compatibility)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub user_token: Option<String>,
55}
56
57/// Export payload structure
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ExportPayload {
60    pub format_version: u32,
61    pub profiles: HashMap<String, ExportProfile>,
62
63    // Allow unknown fields for forward compatibility
64    #[serde(flatten)]
65    #[serde(default)]
66    pub unknown_fields: HashMap<String, serde_json::Value>,
67}
68
69impl ExportPayload {
70    pub fn new() -> Self {
71        Self {
72            format_version: CURRENT_VERSION,
73            profiles: HashMap::new(),
74            unknown_fields: HashMap::new(),
75        }
76    }
77}
78
79impl Default for ExportPayload {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Encoded export data (binary format)
86#[derive(Debug, Clone)]
87pub struct EncodedExport {
88    pub kdf_params: KdfParams,
89    pub encrypted_data: EncryptedData,
90}
91
92/// Encode export payload to binary format
93pub fn encode_export(
94    payload: &ExportPayload,
95    encrypted: &EncryptedData,
96    kdf_params: &KdfParams,
97) -> Result<Vec<u8>> {
98    let mut output = Vec::new();
99
100    // Magic bytes
101    output.extend_from_slice(MAGIC);
102
103    // Format version
104    output.extend_from_slice(&payload.format_version.to_be_bytes());
105
106    // KDF params as JSON
107    let kdf_json = serde_json::json!({
108        "salt": BASE64.encode(&kdf_params.salt),
109        "memory_cost": kdf_params.memory_cost,
110        "time_cost": kdf_params.time_cost,
111        "parallelism": kdf_params.parallelism,
112    });
113    let kdf_bytes = serde_json::to_vec(&kdf_json)?;
114    output.extend_from_slice(&(kdf_bytes.len() as u32).to_be_bytes());
115    output.extend_from_slice(&kdf_bytes);
116
117    // Nonce
118    output.extend_from_slice(&(encrypted.nonce.len() as u32).to_be_bytes());
119    output.extend_from_slice(&encrypted.nonce);
120
121    // Ciphertext
122    output.extend_from_slice(&(encrypted.ciphertext.len() as u32).to_be_bytes());
123    output.extend_from_slice(&encrypted.ciphertext);
124
125    Ok(output)
126}
127
128/// Decode export from binary format
129pub fn decode_export(data: &[u8]) -> Result<EncodedExport> {
130    if data.len() < 8 {
131        return Err(FormatError::InvalidFormat("File too small".to_string()));
132    }
133
134    let mut cursor = 0;
135
136    // Check magic
137    if &data[cursor..cursor + 8] != MAGIC {
138        return Err(FormatError::InvalidMagic);
139    }
140    cursor += 8;
141
142    // Read format version
143    if data.len() < cursor + 4 {
144        return Err(FormatError::InvalidFormat(
145            "Missing format version".to_string(),
146        ));
147    }
148    let version = u32::from_be_bytes([
149        data[cursor],
150        data[cursor + 1],
151        data[cursor + 2],
152        data[cursor + 3],
153    ]);
154    cursor += 4;
155
156    if version != CURRENT_VERSION {
157        return Err(FormatError::UnsupportedVersion(version));
158    }
159
160    // Read KDF params
161    if data.len() < cursor + 4 {
162        return Err(FormatError::InvalidFormat(
163            "Missing KDF params length".to_string(),
164        ));
165    }
166    let kdf_len = u32::from_be_bytes([
167        data[cursor],
168        data[cursor + 1],
169        data[cursor + 2],
170        data[cursor + 3],
171    ]) as usize;
172    cursor += 4;
173
174    if data.len() < cursor + kdf_len {
175        return Err(FormatError::InvalidFormat(
176            "Missing KDF params data".to_string(),
177        ));
178    }
179    let kdf_json: serde_json::Value = serde_json::from_slice(&data[cursor..cursor + kdf_len])?;
180    cursor += kdf_len;
181
182    let salt = BASE64
183        .decode(
184            kdf_json["salt"]
185                .as_str()
186                .ok_or_else(|| FormatError::InvalidFormat("Missing salt".to_string()))?,
187        )
188        .map_err(|e| FormatError::InvalidFormat(format!("Invalid salt: {}", e)))?;
189
190    let kdf_params = KdfParams {
191        salt,
192        memory_cost: kdf_json["memory_cost"]
193            .as_u64()
194            .ok_or_else(|| FormatError::InvalidFormat("Missing memory_cost".to_string()))?
195            as u32,
196        time_cost: kdf_json["time_cost"]
197            .as_u64()
198            .ok_or_else(|| FormatError::InvalidFormat("Missing time_cost".to_string()))?
199            as u32,
200        parallelism: kdf_json["parallelism"]
201            .as_u64()
202            .ok_or_else(|| FormatError::InvalidFormat("Missing parallelism".to_string()))?
203            as u32,
204    };
205
206    // Read nonce
207    if data.len() < cursor + 4 {
208        return Err(FormatError::InvalidFormat(
209            "Missing nonce length".to_string(),
210        ));
211    }
212    let nonce_len = u32::from_be_bytes([
213        data[cursor],
214        data[cursor + 1],
215        data[cursor + 2],
216        data[cursor + 3],
217    ]) as usize;
218    cursor += 4;
219
220    if data.len() < cursor + nonce_len {
221        return Err(FormatError::InvalidFormat("Missing nonce data".to_string()));
222    }
223    let nonce = data[cursor..cursor + nonce_len].to_vec();
224    cursor += nonce_len;
225
226    // Read ciphertext
227    if data.len() < cursor + 4 {
228        return Err(FormatError::InvalidFormat(
229            "Missing ciphertext length".to_string(),
230        ));
231    }
232    let ciphertext_len = u32::from_be_bytes([
233        data[cursor],
234        data[cursor + 1],
235        data[cursor + 2],
236        data[cursor + 3],
237    ]) as usize;
238    cursor += 4;
239
240    if data.len() < cursor + ciphertext_len {
241        return Err(FormatError::InvalidFormat(
242            "Missing ciphertext data".to_string(),
243        ));
244    }
245    let ciphertext = data[cursor..cursor + ciphertext_len].to_vec();
246
247    Ok(EncodedExport {
248        kdf_params,
249        encrypted_data: EncryptedData { nonce, ciphertext },
250    })
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::auth::crypto;
257
258    #[test]
259    fn test_export_payload_serialization() {
260        let mut payload = ExportPayload::new();
261        payload.profiles.insert(
262            "default".to_string(),
263            ExportProfile {
264                team_id: "T123".to_string(),
265                user_id: "U456".to_string(),
266                team_name: Some("Test Team".to_string()),
267                user_name: Some("Test User".to_string()),
268                token: "xoxb-test-token".to_string(),
269                client_id: None,
270                client_secret: None,
271                user_token: None,
272            },
273        );
274
275        let json = serde_json::to_string(&payload).unwrap();
276        let deserialized: ExportPayload = serde_json::from_str(&json).unwrap();
277
278        assert_eq!(payload.format_version, deserialized.format_version);
279        assert_eq!(payload.profiles.len(), deserialized.profiles.len());
280    }
281
282    #[test]
283    fn test_export_payload_unknown_fields() {
284        let json = r#"{
285            "format_version": 1,
286            "profiles": {},
287            "future_field": "some_value"
288        }"#;
289
290        let payload: ExportPayload = serde_json::from_str(json).unwrap();
291        assert_eq!(payload.format_version, 1);
292        assert!(payload.unknown_fields.contains_key("future_field"));
293    }
294
295    #[test]
296    fn test_encode_decode_round_trip() {
297        let payload = ExportPayload::new();
298        let passphrase = "test_password";
299
300        // Create KDF params
301        let kdf_params = KdfParams {
302            salt: crypto::generate_salt(),
303            ..Default::default()
304        };
305
306        // Encrypt payload
307        let payload_json = serde_json::to_vec(&payload).unwrap();
308        let key = crypto::derive_key(passphrase, &kdf_params).unwrap();
309        let encrypted = crypto::encrypt(&payload_json, &key).unwrap();
310
311        // Encode to binary
312        let encoded = encode_export(&payload, &encrypted, &kdf_params).unwrap();
313
314        // Decode from binary
315        let decoded = decode_export(&encoded).unwrap();
316
317        // Verify KDF params match
318        assert_eq!(kdf_params.salt, decoded.kdf_params.salt);
319        assert_eq!(kdf_params.memory_cost, decoded.kdf_params.memory_cost);
320        assert_eq!(kdf_params.time_cost, decoded.kdf_params.time_cost);
321        assert_eq!(kdf_params.parallelism, decoded.kdf_params.parallelism);
322
323        // Verify encrypted data matches
324        assert_eq!(encrypted.nonce, decoded.encrypted_data.nonce);
325        assert_eq!(encrypted.ciphertext, decoded.encrypted_data.ciphertext);
326    }
327
328    #[test]
329    fn test_decode_invalid_magic() {
330        let data = b"INVALID_DATA";
331        let result = decode_export(data);
332        assert!(result.is_err());
333        assert!(matches!(result.unwrap_err(), FormatError::InvalidMagic));
334    }
335
336    #[test]
337    fn test_decode_unsupported_version() {
338        let mut data = Vec::new();
339        data.extend_from_slice(MAGIC);
340        data.extend_from_slice(&999u32.to_be_bytes()); // Unsupported version
341
342        let result = decode_export(&data);
343        assert!(result.is_err());
344        assert!(matches!(
345            result.unwrap_err(),
346            FormatError::UnsupportedVersion(999)
347        ));
348    }
349}