runbeam_sdk/runbeam_api/
token_storage.rs

1use crate::storage::{EncryptedFilesystemStorage, StorageBackend, StorageError};
2use chrono::{DateTime, Utc};
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4
5/// Machine-scoped token for Runbeam Cloud API authentication
6///
7/// This token is issued by Runbeam Cloud and allows the gateway to make
8/// autonomous API calls without user intervention. It has a 30-day expiry.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MachineToken {
11    /// The machine token string
12    pub machine_token: String,
13    /// When the token expires (ISO 8601 format)
14    pub expires_at: String,
15    /// Gateway ID
16    pub gateway_id: String,
17    /// Token abilities/permissions
18    #[serde(default)]
19    pub abilities: Vec<String>,
20    /// When this token was issued/stored (ISO 8601 format)
21    pub issued_at: String,
22}
23
24impl MachineToken {
25    /// Create a new machine token
26    pub fn new(
27        machine_token: String,
28        expires_at: String,
29        gateway_id: String,
30        abilities: Vec<String>,
31    ) -> Self {
32        let issued_at = Utc::now().to_rfc3339();
33
34        Self {
35            machine_token,
36            expires_at,
37            gateway_id,
38            abilities,
39            issued_at,
40        }
41    }
42
43    /// Check if the token has expired
44    pub fn is_expired(&self) -> bool {
45        // Parse the expiry timestamp
46        match DateTime::parse_from_rfc3339(&self.expires_at) {
47            Ok(expiry) => {
48                let now = Utc::now();
49                expiry.with_timezone(&Utc) < now
50            }
51            Err(e) => {
52                tracing::warn!("Failed to parse token expiry date: {}", e);
53                // If we can't parse the date, consider it expired for safety
54                true
55            }
56        }
57    }
58
59    /// Check if the token is still valid (not expired)
60    pub fn is_valid(&self) -> bool {
61        !self.is_expired()
62    }
63}
64
65// ============================================================================
66// Generic Token Storage Functions
67// ============================================================================
68
69/// Save any token using encrypted filesystem storage
70///
71/// Tokens are stored encrypted at `~/.runbeam/<instance_id>/<token_type>.json`
72/// using age encryption. The encryption key is sourced from:
73/// 1. **RUNBEAM_ENCRYPTION_KEY** environment variable (production/containers)
74/// 2. Auto-generated at `~/.runbeam/<instance_id>/encryption.key` (local development)
75///
76/// # Type Parameters
77///
78/// * `T` - Any type that implements `Serialize`
79///
80/// # Arguments
81///
82/// * `instance_id` - Unique identifier for this application instance (e.g., "harmony", "runbeam-cli")
83/// * `token_type` - Token type identifier (e.g., "auth", "user_auth", "custom")
84/// * `token` - The token to save
85///
86/// # Returns
87///
88/// Returns `Ok(())` if the token was saved successfully, or `Err(StorageError)`
89/// if the operation failed.
90///
91/// # Examples
92///
93/// ```no_run
94/// use runbeam_sdk::{save_token, MachineToken, UserToken, UserInfo};
95///
96/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
97/// // Save machine token
98/// let machine_token = MachineToken::new(
99///     "token123".to_string(),
100///     "2024-12-01T00:00:00Z".to_string(),
101///     "gateway-1".to_string(),
102///     vec!["read".to_string()],
103/// );
104/// save_token("harmony", "auth", &machine_token).await?;
105///
106/// // Save user token
107/// let user_info = UserInfo {
108///     id: "user-1".to_string(),
109///     name: "User".to_string(),
110///     email: "user@example.com".to_string(),
111/// };
112/// let user_token = UserToken::new("jwt123".to_string(), Some(3600), Some(user_info));
113/// save_token("runbeam-cli", "user_auth", &user_token).await?;
114/// # Ok(())
115/// # }
116/// ```
117pub async fn save_token<T>(
118    instance_id: &str,
119    token_type: &str,
120    token: &T,
121) -> Result<(), StorageError>
122where
123    T: Serialize,
124{
125    let token_path = format!("runbeam/{}.json", token_type);
126    tracing::debug!(
127        "Saving token: type={}, instance={}, path={}",
128        token_type,
129        instance_id,
130        token_path
131    );
132
133    // Initialize encrypted storage
134    let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
135        .await
136        .map_err(|e| {
137            tracing::error!("Failed to initialize encrypted storage: {}", e);
138            e
139        })?;
140
141    // Serialize token to JSON
142    let json = serde_json::to_vec_pretty(&token).map_err(|e| {
143        tracing::error!("Failed to serialize token: {}", e);
144        StorageError::Config(format!("JSON serialization failed: {}", e))
145    })?;
146
147    // Write to encrypted storage
148    storage.write_file_str(&token_path, &json).await?;
149
150    tracing::info!(
151        "Token saved successfully to encrypted storage: type={}, instance={}",
152        token_type,
153        instance_id
154    );
155
156    Ok(())
157}
158
159/// Load any token from encrypted filesystem storage
160///
161/// Tokens are loaded from `~/.runbeam/<instance_id>/<token_type>.json`
162/// and automatically decrypted using the age encryption key.
163///
164/// # Type Parameters
165///
166/// * `T` - Any type that implements `DeserializeOwned`
167///
168/// # Arguments
169///
170/// * `instance_id` - Unique identifier for this application instance
171/// * `token_type` - Token type identifier (e.g., "auth", "user_auth", "custom")
172///
173/// # Returns
174///
175/// Returns `Ok(Some(token))` if a token was found and loaded successfully,
176/// `Ok(None)` if no token file exists, or `Err(StorageError)` if loading failed.
177///
178/// # Examples
179///
180/// ```no_run
181/// use runbeam_sdk::{load_token, MachineToken, UserToken};
182///
183/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
184/// // Load machine token
185/// let machine_token: Option<MachineToken> = load_token("harmony", "auth").await?;
186///
187/// // Load user token
188/// let user_token: Option<UserToken> = load_token("runbeam-cli", "user_auth").await?;
189/// # Ok(())
190/// # }
191/// ```
192pub async fn load_token<T>(instance_id: &str, token_type: &str) -> Result<Option<T>, StorageError>
193where
194    T: DeserializeOwned,
195{
196    let token_path = format!("runbeam/{}.json", token_type);
197    tracing::debug!(
198        "Loading token: type={}, instance={}, path={}",
199        token_type,
200        instance_id,
201        token_path
202    );
203
204    // Initialize encrypted storage
205    let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
206        .await
207        .map_err(|e| {
208            tracing::debug!("Failed to initialize encrypted storage: {}", e);
209            e
210        })?;
211
212    // Check if token exists
213    if !storage.exists_str(&token_path) {
214        tracing::debug!("No token file found: type={}", token_type);
215        return Ok(None);
216    }
217
218    // Read and decrypt token
219    tracing::debug!("Token found in encrypted filesystem, loading...");
220    let json = storage.read_file_str(&token_path).await?;
221
222    // Deserialize token
223    let token: T = serde_json::from_slice(&json).map_err(|e| {
224        tracing::error!("Failed to deserialize token: {}", e);
225        StorageError::Config(format!("JSON deserialization failed: {}", e))
226    })?;
227
228    tracing::debug!(
229        "Token loaded successfully from encrypted filesystem: type={}",
230        token_type
231    );
232    Ok(Some(token))
233}
234
235/// Clear any token from encrypted filesystem storage
236///
237/// Removes the token file from `~/.runbeam/<instance_id>/<token_type>.json`.
238///
239/// # Arguments
240///
241/// * `instance_id` - Unique identifier for this application instance
242/// * `token_type` - Token type identifier (e.g., "auth", "user_auth", "custom")
243///
244/// # Returns
245///
246/// Returns `Ok(())` if the token was cleared successfully or didn't exist,
247/// or `Err(StorageError)` if the operation failed.
248///
249/// # Examples
250///
251/// ```no_run
252/// use runbeam_sdk::clear_token;
253///
254/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
255/// // Clear machine token
256/// clear_token("harmony", "auth").await?;
257///
258/// // Clear user token
259/// clear_token("runbeam-cli", "user_auth").await?;
260/// # Ok(())
261/// # }
262/// ```
263pub async fn clear_token(instance_id: &str, token_type: &str) -> Result<(), StorageError> {
264    let token_path = format!("runbeam/{}.json", token_type);
265    tracing::debug!(
266        "Clearing token: type={}, instance={}, path={}",
267        token_type,
268        instance_id,
269        token_path
270    );
271
272    // Initialize encrypted storage
273    let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
274        .await
275        .map_err(|e| {
276            tracing::debug!("Failed to initialize encrypted storage: {}", e);
277            e
278        })?;
279
280    // Check if token exists
281    if !storage.exists_str(&token_path) {
282        tracing::debug!("No token file to clear: type={}", token_type);
283        return Ok(());
284    }
285
286    // Remove token
287    tracing::debug!("Clearing token from encrypted filesystem storage");
288    storage.remove_str(&token_path).await.map_err(|e| {
289        tracing::error!("Failed to clear token: {}", e);
290        e
291    })?;
292
293    tracing::info!("Token cleared successfully: type={}", token_type);
294    Ok(())
295}
296
297// ============================================================================
298// Backwards-Compatible Machine Token Functions
299// ============================================================================
300
301/// Save a machine token with an explicit encryption key
302///
303/// This function uses the provided encryption key for token storage.
304///
305/// # Arguments
306///
307/// * `instance_id` - Unique identifier for this application instance (e.g., "harmony", "runbeam-cli", "test-123")
308/// * `token` - The machine token to save
309/// * `encryption_key` - Base64-encoded age X25519 encryption key for filesystem storage
310///
311/// # Returns
312///
313/// Returns `Ok(())` if the token was saved successfully, or `Err(StorageError)`
314/// if the operation failed.
315pub async fn save_token_with_key(
316    instance_id: &str,
317    token: &MachineToken,
318    encryption_key: &str,
319) -> Result<(), StorageError> {
320    let token_path = "runbeam/auth.json";
321    tracing::debug!(
322        "Saving machine token with explicit encryption key: gateway={}, instance={}",
323        token.gateway_id,
324        instance_id
325    );
326
327    // Use encrypted filesystem with the provided key
328    let storage =
329        EncryptedFilesystemStorage::new_with_instance_and_key(instance_id, encryption_key).await?;
330
331    // Serialize token to JSON
332    let json = serde_json::to_vec_pretty(&token).map_err(|e| {
333        tracing::error!("Failed to serialize machine token: {}", e);
334        StorageError::Config(format!("JSON serialization failed: {}", e))
335    })?;
336
337    // Write to encrypted storage
338    storage.write_file_str(token_path, &json).await?;
339
340    tracing::info!(
341        "Machine token saved successfully with explicit key: gateway_id={}, expires_at={}",
342        token.gateway_id,
343        token.expires_at
344    );
345
346    Ok(())
347}
348
349/// Save a machine token using encrypted filesystem storage
350///
351/// **Backwards-compatible wrapper** for `save_token(instance_id, "auth", token)`.
352///
353/// Tokens are stored encrypted at `~/.runbeam/<instance_id>/auth.json`.
354///
355/// # Encryption
356///
357/// The encryption key is sourced from:
358/// 1. `RUNBEAM_ENCRYPTION_KEY` environment variable (base64-encoded)
359/// 2. Auto-generated key stored at `~/.runbeam/<instance_id>/encryption.key` (0600 permissions)
360///
361/// # Arguments
362///
363/// * `instance_id` - Unique identifier for this application instance (e.g., "harmony", "runbeam-cli", "test-123")
364/// * `token` - The machine token to save
365///
366/// # Returns
367///
368/// Returns `Ok(())` if the token was saved successfully, or `Err(StorageError)`
369/// if the operation failed.
370pub async fn save_machine_token(
371    instance_id: &str,
372    token: &MachineToken,
373) -> Result<(), StorageError> {
374    save_token(instance_id, "auth", token).await
375}
376
377/// Load a machine token from encrypted filesystem storage
378///
379/// **Backwards-compatible wrapper** for `load_token(instance_id, "auth")`.
380///
381/// Attempts to load the token from `~/.runbeam/<instance_id>/auth.json`. Returns `None` if the
382/// file doesn't exist.
383///
384/// # Arguments
385///
386/// * `instance_id` - Unique identifier for this application instance (e.g., "harmony", "runbeam-cli")
387///
388/// # Returns
389///
390/// Returns `Ok(Some(token))` if a token was found and loaded successfully,
391/// `Ok(None)` if no token file exists, or `Err(StorageError)` if loading failed.
392pub async fn load_machine_token(instance_id: &str) -> Result<Option<MachineToken>, StorageError> {
393    load_token(instance_id, "auth").await
394}
395
396/// Clear the machine token from encrypted filesystem storage
397///
398/// **Backwards-compatible wrapper** for `clear_token(instance_id, "auth")`.
399///
400/// Removes the token file at `~/.runbeam/<instance_id>/auth.json` if it exists.
401///
402/// # Arguments
403///
404/// * `instance_id` - Unique identifier for this application instance (e.g., "harmony", "runbeam-cli")
405///
406/// # Returns
407///
408/// Returns `Ok(())` if the token was cleared successfully or didn't exist,
409/// or `Err(StorageError)` if the operation failed.
410pub async fn clear_machine_token(instance_id: &str) -> Result<(), StorageError> {
411    clear_token(instance_id, "auth").await
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use serial_test::serial;
418
419    /// Setup test encryption key and return cleanup function
420    fn setup_test_encryption() -> impl Drop {
421        use base64::Engine;
422        use secrecy::ExposeSecret;
423        use std::env;
424
425        let identity = age::x25519::Identity::generate();
426        let key_base64 = base64::engine::general_purpose::STANDARD
427            .encode(identity.to_string().expose_secret().as_bytes());
428        env::set_var("RUNBEAM_ENCRYPTION_KEY", &key_base64);
429
430        // Return a guard that will clean up on drop
431        struct Guard;
432        impl Drop for Guard {
433            fn drop(&mut self) {
434                std::env::remove_var("RUNBEAM_ENCRYPTION_KEY");
435            }
436        }
437        Guard
438    }
439
440    #[test]
441    fn test_machine_token_creation() {
442        let token = MachineToken::new(
443            "test_token".to_string(),
444            "2025-12-31T23:59:59Z".to_string(),
445            "gw123".to_string(),
446            vec!["harmony:send".to_string(), "harmony:receive".to_string()],
447        );
448
449        assert_eq!(token.machine_token, "test_token");
450        assert_eq!(token.gateway_id, "gw123");
451        assert_eq!(token.abilities.len(), 2);
452        assert!(!token.issued_at.is_empty());
453    }
454
455    #[test]
456    fn test_machine_token_is_expired() {
457        // Expired token (year 2020)
458        let expired_token = MachineToken::new(
459            "test_token".to_string(),
460            "2020-01-01T00:00:00Z".to_string(),
461            "gw123".to_string(),
462            vec![],
463        );
464        assert!(expired_token.is_expired());
465        assert!(!expired_token.is_valid());
466
467        // Valid token (far future)
468        let valid_token = MachineToken::new(
469            "test_token".to_string(),
470            "2099-12-31T23:59:59Z".to_string(),
471            "gw123".to_string(),
472            vec![],
473        );
474        assert!(!valid_token.is_expired());
475        assert!(valid_token.is_valid());
476    }
477
478    #[test]
479    fn test_machine_token_serialization() {
480        let token = MachineToken::new(
481            "test_token".to_string(),
482            "2025-12-31T23:59:59Z".to_string(),
483            "gw123".to_string(),
484            vec!["harmony:send".to_string()],
485        );
486
487        let json = serde_json::to_string(&token).unwrap();
488        assert!(json.contains("\"machine_token\":\"test_token\""));
489        assert!(json.contains("\"gateway_id\":\"gw123\""));
490
491        // Deserialize and verify
492        let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
493        assert_eq!(deserialized.machine_token, token.machine_token);
494        assert_eq!(deserialized.gateway_id, token.gateway_id);
495    }
496
497    #[tokio::test]
498    #[serial]
499    async fn test_save_and_load_token_secure() {
500        let _guard = setup_test_encryption();
501        let instance_id = "test-save-load";
502        // Clear any existing token first
503        let _ = clear_machine_token(instance_id).await;
504
505        let token = MachineToken::new(
506            "test_token_secure".to_string(),
507            "2099-12-31T23:59:59Z".to_string(),
508            "gw_test".to_string(),
509            vec!["harmony:send".to_string()],
510        );
511
512        // Save token using automatic secure storage (wrapper)
513        save_machine_token(instance_id, &token).await.unwrap();
514
515        // Load token using automatic secure storage (wrapper)
516        let loaded = load_machine_token(instance_id).await.unwrap();
517        assert!(loaded.is_some());
518
519        let loaded_token = loaded.unwrap();
520        assert_eq!(loaded_token.machine_token, token.machine_token);
521        assert_eq!(loaded_token.gateway_id, token.gateway_id);
522        assert!(loaded_token.is_valid());
523
524        // Cleanup
525        clear_machine_token(instance_id).await.unwrap();
526    }
527
528    #[tokio::test]
529    #[serial]
530    async fn test_load_nonexistent_token_secure() {
531        let _guard = setup_test_encryption();
532        let instance_id = "test-nonexistent";
533        // Clear any existing token
534        let _ = clear_machine_token(instance_id).await;
535
536        // Load from empty storage should return None
537        let result = load_machine_token(instance_id).await.unwrap();
538        assert!(result.is_none());
539    }
540
541    #[tokio::test]
542    #[serial]
543    async fn test_clear_token_secure() {
544        let _guard = setup_test_encryption();
545        let instance_id = "test-clear";
546        // Clear any existing token first
547        let _ = clear_machine_token(instance_id).await;
548
549        let token = MachineToken::new(
550            "test_clear".to_string(),
551            "2099-12-31T23:59:59Z".to_string(),
552            "gw_clear".to_string(),
553            vec![],
554        );
555
556        // Save token
557        save_machine_token(instance_id, &token).await.unwrap();
558
559        // Verify it exists
560        assert!(load_machine_token(instance_id).await.unwrap().is_some());
561
562        // Clear token
563        clear_machine_token(instance_id).await.unwrap();
564
565        // Verify it's gone
566        assert!(load_machine_token(instance_id).await.unwrap().is_none());
567    }
568
569    #[tokio::test]
570    #[serial]
571    async fn test_clear_nonexistent_token_secure() {
572        let _guard = setup_test_encryption();
573        let instance_id = "test-clear-nonexistent";
574        // Clear token that doesn't exist should not error
575        clear_machine_token(instance_id).await.unwrap();
576    }
577
578    #[tokio::test]
579    #[serial]
580    async fn test_token_expiry_detection() {
581        let _guard = setup_test_encryption();
582        let instance_id = "test-expiry";
583        let _ = clear_machine_token(instance_id).await;
584
585        // Create expired token
586        let expired_token = MachineToken::new(
587            "expired_token".to_string(),
588            "2020-01-01T00:00:00Z".to_string(),
589            "gw_expired".to_string(),
590            vec![],
591        );
592
593        save_machine_token(instance_id, &expired_token)
594            .await
595            .unwrap();
596
597        // Load and verify it's marked as expired
598        let loaded = load_machine_token(instance_id).await.unwrap();
599        assert!(loaded.is_some());
600        let loaded_token = loaded.unwrap();
601        assert!(loaded_token.is_expired());
602        assert!(!loaded_token.is_valid());
603
604        // Cleanup
605        clear_machine_token(instance_id).await.unwrap();
606    }
607
608    #[tokio::test]
609    #[serial]
610    async fn test_token_with_abilities() {
611        let _guard = setup_test_encryption();
612        let instance_id = "test-abilities";
613        let _ = clear_machine_token(instance_id).await;
614
615        let token = MachineToken::new(
616            "token_with_abilities".to_string(),
617            "2099-12-31T23:59:59Z".to_string(),
618            "gw_abilities".to_string(),
619            vec![
620                "harmony:send".to_string(),
621                "harmony:receive".to_string(),
622                "harmony:config".to_string(),
623            ],
624        );
625
626        save_machine_token(instance_id, &token).await.unwrap();
627
628        let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
629        assert_eq!(loaded.abilities.len(), 3);
630        assert!(loaded.abilities.contains(&"harmony:send".to_string()));
631        assert!(loaded.abilities.contains(&"harmony:receive".to_string()));
632        assert!(loaded.abilities.contains(&"harmony:config".to_string()));
633
634        // Cleanup
635        clear_machine_token(instance_id).await.unwrap();
636    }
637
638    #[tokio::test]
639    #[serial]
640    async fn test_token_overwrites_existing() {
641        let _guard = setup_test_encryption();
642        let instance_id = "test-overwrite";
643        let _ = clear_machine_token(instance_id).await;
644
645        // Save first token
646        let token1 = MachineToken::new(
647            "first_token".to_string(),
648            "2099-12-31T23:59:59Z".to_string(),
649            "gw_first".to_string(),
650            vec![],
651        );
652        save_machine_token(instance_id, &token1).await.unwrap();
653
654        // Save second token (should overwrite)
655        let token2 = MachineToken::new(
656            "second_token".to_string(),
657            "2099-12-31T23:59:59Z".to_string(),
658            "gw_second".to_string(),
659            vec![],
660        );
661        save_machine_token(instance_id, &token2).await.unwrap();
662
663        // Should load second token
664        let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
665        assert_eq!(loaded.machine_token, "second_token");
666        assert_eq!(loaded.gateway_id, "gw_second");
667
668        // Cleanup
669        clear_machine_token(instance_id).await.unwrap();
670    }
671
672    #[tokio::test]
673    async fn test_token_encrypted_on_disk() {
674        use crate::storage::EncryptedFilesystemStorage;
675        use tempfile::TempDir;
676
677        let instance_id = "test-encryption-verify";
678        let temp_dir = TempDir::new().unwrap();
679
680        // Create token with sensitive data
681        let token = MachineToken::new(
682            "super_secret_token_12345".to_string(),
683            "2099-12-31T23:59:59Z".to_string(),
684            "gw_secret".to_string(),
685            vec!["harmony:admin".to_string()],
686        );
687
688        // Use encrypted storage directly for testing
689        let storage_path = temp_dir.path().join(instance_id);
690        let storage = EncryptedFilesystemStorage::new(&storage_path)
691            .await
692            .unwrap();
693
694        // Save token
695        let token_json = serde_json::to_vec(&token).unwrap();
696        storage
697            .write_file_str("auth.json", &token_json)
698            .await
699            .unwrap();
700
701        // Find the stored token file
702        let token_path = storage_path.join("auth.json");
703
704        // Verify file exists
705        assert!(
706            token_path.exists(),
707            "Token file should exist at {:?}",
708            token_path
709        );
710
711        // Read raw file contents
712        let raw_contents = std::fs::read(&token_path).unwrap();
713        let raw_string = String::from_utf8_lossy(&raw_contents);
714
715        // Verify the file does NOT contain plaintext sensitive data
716        assert!(
717            !raw_string.contains("super_secret_token_12345"),
718            "Token file should NOT contain plaintext token: {}",
719            raw_string
720        );
721        assert!(
722            !raw_string.contains("gw_secret"),
723            "Token file should NOT contain plaintext gateway_id: {}",
724            raw_string
725        );
726        assert!(
727            !raw_string.contains("harmony:admin"),
728            "Token file should NOT contain plaintext abilities: {}",
729            raw_string
730        );
731
732        // Verify it contains age encryption markers
733        if raw_contents.len() > 50 {
734            // age encryption typically starts with "age-encryption.org/v1"
735            let has_age_header = raw_string.starts_with("age-encryption.org/v1");
736            assert!(
737                has_age_header || raw_contents.starts_with(b"age-encryption.org/v1"),
738                "File should contain age encryption header. Raw contents (first 100 bytes): {:?}",
739                &raw_contents[..std::cmp::min(100, raw_contents.len())]
740            );
741        }
742
743        // Verify we can still decrypt and load the token correctly
744        let decrypted_data = storage.read_file_str("auth.json").await.unwrap();
745        let loaded_token: MachineToken = serde_json::from_slice(&decrypted_data).unwrap();
746        assert_eq!(loaded_token.machine_token, "super_secret_token_12345");
747        assert_eq!(loaded_token.gateway_id, "gw_secret");
748    }
749
750    #[tokio::test]
751    async fn test_token_file_cannot_be_read_as_json() {
752        use crate::storage::EncryptedFilesystemStorage;
753        use tempfile::TempDir;
754
755        let instance_id = "test-raw-json-read";
756        let temp_dir = TempDir::new().unwrap();
757        let storage_path = temp_dir.path().join(instance_id);
758
759        let token = MachineToken::new(
760            "test_token_json".to_string(),
761            "2099-12-31T23:59:59Z".to_string(),
762            "gw_json".to_string(),
763            vec![],
764        );
765
766        // Use encrypted storage directly
767        let storage = EncryptedFilesystemStorage::new(&storage_path)
768            .await
769            .unwrap();
770        let token_json = serde_json::to_vec(&token).unwrap();
771        storage
772            .write_file_str("auth.json", &token_json)
773            .await
774            .unwrap();
775
776        // Try to read the file as JSON
777        let token_path = storage_path.join("auth.json");
778
779        // Read as bytes first (encrypted data may not be valid UTF-8)
780        let raw_contents = std::fs::read(&token_path).unwrap();
781
782        // Try to parse as JSON - should fail because it's encrypted
783        let json_parse_result: Result<serde_json::Value, _> = serde_json::from_slice(&raw_contents);
784        assert!(
785            json_parse_result.is_err(),
786            "Raw token file should NOT be parseable as JSON (it should be encrypted)"
787        );
788    }
789
790    #[tokio::test]
791    async fn test_token_different_from_plaintext() {
792        use crate::storage::EncryptedFilesystemStorage;
793        use tempfile::TempDir;
794
795        let instance_id = "test-plaintext-compare";
796        let temp_dir = TempDir::new().unwrap();
797        let storage_path = temp_dir.path().join(instance_id);
798
799        let token = MachineToken::new(
800            "comparison_token".to_string(),
801            "2099-12-31T23:59:59Z".to_string(),
802            "gw_compare".to_string(),
803            vec!["test:ability".to_string()],
804        );
805
806        // Get plaintext JSON representation
807        let plaintext_json = serde_json::to_vec(&token).unwrap();
808
809        // Use encrypted storage directly
810        let storage = EncryptedFilesystemStorage::new(&storage_path)
811            .await
812            .unwrap();
813        storage
814            .write_file_str("auth.json", &plaintext_json)
815            .await
816            .unwrap();
817
818        // Read encrypted file
819        let token_path = storage_path.join("auth.json");
820        let encrypted_contents = std::fs::read(&token_path).unwrap();
821
822        // Encrypted contents should be different from plaintext
823        assert_ne!(
824            encrypted_contents, plaintext_json,
825            "Encrypted file contents should differ from plaintext JSON"
826        );
827
828        // Encrypted contents should be longer (encryption overhead)
829        assert!(
830            encrypted_contents.len() > plaintext_json.len(),
831            "Encrypted file should be larger due to encryption overhead. Encrypted: {}, Plaintext: {}",
832            encrypted_contents.len(),
833            plaintext_json.len()
834        );
835    }
836
837    #[tokio::test]
838    async fn test_multiple_instances_isolated() {
839        use crate::storage::EncryptedFilesystemStorage;
840        use tempfile::TempDir;
841
842        let temp_dir = TempDir::new().unwrap();
843
844        // Create tokens for different instances
845        let token1 = MachineToken::new(
846            "token_instance_1".to_string(),
847            "2099-12-31T23:59:59Z".to_string(),
848            "gw_1".to_string(),
849            vec![],
850        );
851
852        let token2 = MachineToken::new(
853            "token_instance_2".to_string(),
854            "2099-12-31T23:59:59Z".to_string(),
855            "gw_2".to_string(),
856            vec![],
857        );
858
859        // Create separate storage instances
860        let storage1_path = temp_dir.path().join("instance-1");
861        let storage2_path = temp_dir.path().join("instance-2");
862
863        let storage1 = EncryptedFilesystemStorage::new(&storage1_path)
864            .await
865            .unwrap();
866        let storage2 = EncryptedFilesystemStorage::new(&storage2_path)
867            .await
868            .unwrap();
869
870        // Save tokens
871        let token1_json = serde_json::to_vec(&token1).unwrap();
872        let token2_json = serde_json::to_vec(&token2).unwrap();
873        storage1
874            .write_file_str("auth.json", &token1_json)
875            .await
876            .unwrap();
877        storage2
878            .write_file_str("auth.json", &token2_json)
879            .await
880            .unwrap();
881
882        // Verify files are in separate directories
883        let path1 = storage1_path.join("auth.json");
884        let path2 = storage2_path.join("auth.json");
885
886        assert!(path1.exists(), "Instance 1 token file should exist");
887        assert!(path2.exists(), "Instance 2 token file should exist");
888        assert_ne!(path1, path2, "Token files should be in different locations");
889
890        // Verify encryption keys are separate (if not using env var)
891        let key1_path = storage1_path.join("encryption.key");
892        let key2_path = storage2_path.join("encryption.key");
893
894        if key1_path.exists() && key2_path.exists() {
895            let key1_contents = std::fs::read(&key1_path).unwrap();
896            let key2_contents = std::fs::read(&key2_path).unwrap();
897            assert_ne!(
898                key1_contents, key2_contents,
899                "Encryption keys should be different for each instance"
900            );
901        }
902
903        // Verify tokens are isolated by decrypting them
904        let decrypted1 = storage1.read_file_str("auth.json").await.unwrap();
905        let decrypted2 = storage2.read_file_str("auth.json").await.unwrap();
906
907        let loaded1: MachineToken = serde_json::from_slice(&decrypted1).unwrap();
908        let loaded2: MachineToken = serde_json::from_slice(&decrypted2).unwrap();
909
910        assert_eq!(loaded1.machine_token, "token_instance_1");
911        assert_eq!(loaded1.gateway_id, "gw_1");
912        assert_eq!(loaded2.machine_token, "token_instance_2");
913        assert_eq!(loaded2.gateway_id, "gw_2");
914    }
915
916    #[tokio::test]
917    #[cfg(unix)]
918    async fn test_encryption_key_file_permissions() {
919        use crate::storage::EncryptedFilesystemStorage;
920        use std::os::unix::fs::PermissionsExt;
921        use tempfile::TempDir;
922
923        let instance_id = "test-key-permissions";
924        let temp_dir = TempDir::new().unwrap();
925        let storage_path = temp_dir.path().join(instance_id);
926
927        // Create storage which will generate encryption key
928        let _storage = EncryptedFilesystemStorage::new(&storage_path)
929            .await
930            .unwrap();
931
932        // Check encryption key file permissions
933        let key_path = storage_path.join("encryption.key");
934
935        // Key may be in storage path or env var - check if file was created
936        if !key_path.exists() {
937            // If using RUNBEAM_ENCRYPTION_KEY env var, skip this test
938            return;
939        }
940
941        let metadata = std::fs::metadata(&key_path).unwrap();
942        let permissions = metadata.permissions();
943        let mode = permissions.mode();
944
945        // Check that only owner has read/write (0600)
946        let permission_bits = mode & 0o777;
947        assert_eq!(
948            permission_bits, 0o600,
949            "Encryption key file should have 0600 permissions (owner read/write only), got {:o}",
950            permission_bits
951        );
952    }
953
954    #[tokio::test]
955    async fn test_tampered_token_file_fails_to_load() {
956        use crate::storage::EncryptedFilesystemStorage;
957        use tempfile::TempDir;
958
959        let instance_id = "test-tamper";
960        let temp_dir = TempDir::new().unwrap();
961        let storage_path = temp_dir.path().join(instance_id);
962
963        let token = MachineToken::new(
964            "original_token".to_string(),
965            "2099-12-31T23:59:59Z".to_string(),
966            "gw_tamper".to_string(),
967            vec![],
968        );
969
970        // Use encrypted storage directly
971        let storage = EncryptedFilesystemStorage::new(&storage_path)
972            .await
973            .unwrap();
974        let token_json = serde_json::to_vec(&token).unwrap();
975        storage
976            .write_file_str("auth.json", &token_json)
977            .await
978            .unwrap();
979
980        // Tamper with the encrypted file
981        let token_path = storage_path.join("auth.json");
982        let mut contents = std::fs::read(&token_path).unwrap();
983
984        // Flip some bytes in the middle of the file
985        if contents.len() > 50 {
986            contents[25] = contents[25].wrapping_add(1);
987            contents[30] = contents[30].wrapping_sub(1);
988            std::fs::write(&token_path, contents).unwrap();
989        }
990
991        // Attempting to decrypt should fail
992        let result = storage.read_file_str("auth.json").await;
993        assert!(
994            result.is_err(),
995            "Loading tampered encrypted file should fail"
996        );
997    }
998
999    // ========================================================================
1000    // Generic Token Storage Tests
1001    // ========================================================================
1002
1003    #[tokio::test]
1004    #[serial]
1005    async fn test_generic_save_and_load_user_token() {
1006        use crate::runbeam_api::types::UserToken;
1007        let _guard = setup_test_encryption();
1008        let instance_id = "test-user-token";
1009        clear_token(instance_id, "user_auth").await.ok();
1010
1011        let user_token = UserToken::new(
1012            "user_jwt_token".to_string(),
1013            Some(1234567890),
1014            Some(crate::runbeam_api::types::UserInfo {
1015                id: "user123".to_string(),
1016                name: "Test User".to_string(),
1017                email: "test@example.com".to_string(),
1018            }),
1019        );
1020
1021        // Save using generic function
1022        save_token(instance_id, "user_auth", &user_token)
1023            .await
1024            .unwrap();
1025
1026        // Load using generic function
1027        let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1028        assert!(loaded.is_some());
1029
1030        let loaded_token = loaded.unwrap();
1031        assert_eq!(loaded_token.token, user_token.token);
1032        assert_eq!(loaded_token.expires_at, user_token.expires_at);
1033        assert!(loaded_token.user.is_some());
1034
1035        // Cleanup
1036        clear_token(instance_id, "user_auth").await.unwrap();
1037    }
1038
1039    #[tokio::test]
1040    #[serial]
1041    async fn test_different_token_types_isolated() {
1042        use crate::runbeam_api::types::UserToken;
1043        let _guard = setup_test_encryption();
1044        let instance_id = "test-isolation";
1045
1046        // Create different token types
1047        let user_token = UserToken::new("user_token".to_string(), None, None);
1048
1049        let machine_token = MachineToken::new(
1050            "machine_token".to_string(),
1051            "2099-12-31T23:59:59Z".to_string(),
1052            "gw_test".to_string(),
1053            vec![],
1054        );
1055
1056        // Save both
1057        save_token(instance_id, "user_auth", &user_token)
1058            .await
1059            .unwrap();
1060        save_token(instance_id, "auth", &machine_token)
1061            .await
1062            .unwrap();
1063
1064        // Load both - should be independent
1065        let loaded_user: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1066        let loaded_machine: Option<MachineToken> = load_token(instance_id, "auth").await.unwrap();
1067
1068        assert!(loaded_user.is_some());
1069        assert!(loaded_machine.is_some());
1070        assert_eq!(loaded_user.unwrap().token, "user_token");
1071        assert_eq!(loaded_machine.unwrap().machine_token, "machine_token");
1072
1073        // Cleanup
1074        clear_token(instance_id, "user_auth").await.unwrap();
1075        clear_token(instance_id, "auth").await.unwrap();
1076    }
1077
1078    #[tokio::test]
1079    #[serial]
1080    async fn test_user_token_with_full_metadata() {
1081        use crate::runbeam_api::types::UserToken;
1082        let _guard = setup_test_encryption();
1083        let instance_id = "test-user-full";
1084        clear_token(instance_id, "user_auth").await.ok();
1085
1086        let user_token = UserToken::new(
1087            "detailed_user_token".to_string(),
1088            Some(2000000000),
1089            Some(crate::runbeam_api::types::UserInfo {
1090                id: "user456".to_string(),
1091                name: "John Doe".to_string(),
1092                email: "john@example.com".to_string(),
1093            }),
1094        );
1095
1096        // Save and load
1097        save_token(instance_id, "user_auth", &user_token)
1098            .await
1099            .unwrap();
1100        let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1101
1102        assert!(loaded.is_some());
1103        let loaded_token = loaded.unwrap();
1104        assert_eq!(loaded_token.token, "detailed_user_token");
1105        assert_eq!(loaded_token.expires_at, Some(2000000000));
1106
1107        let user_info = loaded_token.user.unwrap();
1108        assert_eq!(user_info.id, "user456");
1109        assert_eq!(user_info.name, "John Doe");
1110        assert_eq!(user_info.email, "john@example.com");
1111
1112        // Cleanup
1113        clear_token(instance_id, "user_auth").await.unwrap();
1114    }
1115}