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}
53
54/// Export payload structure
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ExportPayload {
57    pub format_version: u32,
58    pub profiles: HashMap<String, ExportProfile>,
59
60    // Allow unknown fields for forward compatibility
61    #[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/// Encoded export data (binary format)
83#[derive(Debug, Clone)]
84pub struct EncodedExport {
85    pub kdf_params: KdfParams,
86    pub encrypted_data: EncryptedData,
87}
88
89/// Encode export payload to binary format
90pub 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    // Magic bytes
98    output.extend_from_slice(MAGIC);
99
100    // Format version
101    output.extend_from_slice(&payload.format_version.to_be_bytes());
102
103    // KDF params as JSON
104    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    // Nonce
115    output.extend_from_slice(&(encrypted.nonce.len() as u32).to_be_bytes());
116    output.extend_from_slice(&encrypted.nonce);
117
118    // Ciphertext
119    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
125/// Decode export from binary format
126pub 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    // Check magic
134    if &data[cursor..cursor + 8] != MAGIC {
135        return Err(FormatError::InvalidMagic);
136    }
137    cursor += 8;
138
139    // Read format version
140    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    // Read KDF params
158    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    // Read nonce
204    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    // Read ciphertext
224    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        // Create KDF params
297        let kdf_params = KdfParams {
298            salt: crypto::generate_salt(),
299            ..Default::default()
300        };
301
302        // Encrypt payload
303        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        // Encode to binary
308        let encoded = encode_export(&payload, &encrypted, &kdf_params).unwrap();
309
310        // Decode from binary
311        let decoded = decode_export(&encoded).unwrap();
312
313        // Verify KDF params match
314        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        // Verify encrypted data matches
320        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()); // Unsupported version
337
338        let result = decode_export(&data);
339        assert!(result.is_err());
340        assert!(matches!(
341            result.unwrap_err(),
342            FormatError::UnsupportedVersion(999)
343        ));
344    }
345}