1use crate::storage::{EncryptedFilesystemStorage, KeyringStorage, StorageBackend, StorageError};
2use chrono::{DateTime, Utc};
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4
5enum StorageBackendType {
7 Keyring(KeyringStorage),
8 Encrypted(EncryptedFilesystemStorage),
9}
10
11async fn get_storage_backend(
21 instance_id: &str,
22 token_path: &str,
23) -> Result<StorageBackendType, StorageError> {
24 let keyring_disabled = std::env::var("RUNBEAM_DISABLE_KEYRING").is_ok();
26
27 let keyring_works = if keyring_disabled {
29 tracing::debug!("Keyring disabled via RUNBEAM_DISABLE_KEYRING environment variable");
30 false
31 } else {
32 let keyring = KeyringStorage::new("runbeam");
33 let path = token_path.to_string();
34
35 let test_result = tokio::task::spawn_blocking(move || keyring.exists_str(&path)).await;
37
38 match test_result {
39 Ok(_) => {
40 tracing::debug!(
42 "Keyring storage is available, using OS keychain for secure token storage"
43 );
44 true
45 }
46 Err(e) => {
47 tracing::debug!(
48 "Keyring storage is unavailable ({}), falling back to encrypted filesystem storage",
49 e
50 );
51 false
52 }
53 }
54 };
55
56 if keyring_works {
57 Ok(StorageBackendType::Keyring(KeyringStorage::new("runbeam")))
58 } else {
59 tracing::debug!(
60 "Using encrypted filesystem storage at ~/.runbeam/{} (RUNBEAM_ENCRYPTION_KEY environment variable can override key)",
61 instance_id
62 );
63 let encrypted = EncryptedFilesystemStorage::new_with_instance(instance_id)
64 .await
65 .map_err(|e| {
66 tracing::error!("Failed to initialize encrypted storage: {}", e);
67 e
68 })?;
69 Ok(StorageBackendType::Encrypted(encrypted))
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct MachineToken {
79 pub machine_token: String,
81 pub expires_at: String,
83 pub gateway_id: String,
85 pub gateway_code: String,
87 #[serde(default)]
89 pub abilities: Vec<String>,
90 pub issued_at: String,
92}
93
94impl MachineToken {
95 pub fn new(
97 machine_token: String,
98 expires_at: String,
99 gateway_id: String,
100 gateway_code: String,
101 abilities: Vec<String>,
102 ) -> Self {
103 let issued_at = Utc::now().to_rfc3339();
104
105 Self {
106 machine_token,
107 expires_at,
108 gateway_id,
109 gateway_code,
110 abilities,
111 issued_at,
112 }
113 }
114
115 pub fn is_expired(&self) -> bool {
117 match DateTime::parse_from_rfc3339(&self.expires_at) {
119 Ok(expiry) => {
120 let now = Utc::now();
121 expiry.with_timezone(&Utc) < now
122 }
123 Err(e) => {
124 tracing::warn!("Failed to parse token expiry date: {}", e);
125 true
127 }
128 }
129 }
130
131 pub fn is_valid(&self) -> bool {
133 !self.is_expired()
134 }
135}
136
137pub async fn save_token<T>(
190 instance_id: &str,
191 token_type: &str,
192 token: &T,
193) -> Result<(), StorageError>
194where
195 T: Serialize,
196{
197 let token_path = format!("runbeam/{}.json", token_type);
198 tracing::debug!(
199 "Saving token: type={}, instance={}, path={}",
200 token_type,
201 instance_id,
202 token_path
203 );
204
205 let backend = get_storage_backend(instance_id, &token_path).await?;
206
207 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
209 tracing::error!("Failed to serialize token: {}", e);
210 StorageError::Config(format!("JSON serialization failed: {}", e))
211 })?;
212
213 match backend {
215 StorageBackendType::Keyring(storage) => storage.write_file_str(&token_path, &json).await?,
216 StorageBackendType::Encrypted(storage) => {
217 storage.write_file_str(&token_path, &json).await?
218 }
219 }
220
221 tracing::info!(
222 "Token saved successfully: type={}, instance={}",
223 token_type,
224 instance_id
225 );
226
227 Ok(())
228}
229
230pub async fn load_token<T>(instance_id: &str, token_type: &str) -> Result<Option<T>, StorageError>
265where
266 T: DeserializeOwned,
267{
268 let token_path = format!("runbeam/{}.json", token_type);
269 tracing::debug!(
270 "Loading token: type={}, instance={}, path={}",
271 token_type,
272 instance_id,
273 token_path
274 );
275
276 let backend = get_storage_backend(instance_id, &token_path).await?;
277
278 let exists = match &backend {
280 StorageBackendType::Keyring(storage) => storage.exists_str(&token_path),
281 StorageBackendType::Encrypted(storage) => storage.exists_str(&token_path),
282 };
283
284 if !exists {
285 tracing::debug!("No token file found: type={}", token_type);
286 return Ok(None);
287 }
288
289 let json = match backend {
291 StorageBackendType::Keyring(storage) => storage.read_file_str(&token_path).await?,
292 StorageBackendType::Encrypted(storage) => storage.read_file_str(&token_path).await?,
293 };
294
295 let token: T = serde_json::from_slice(&json).map_err(|e| {
297 tracing::error!("Failed to deserialize token: {}", e);
298 StorageError::Config(format!("JSON deserialization failed: {}", e))
299 })?;
300
301 tracing::debug!("Token loaded successfully: type={}", token_type);
302
303 Ok(Some(token))
304}
305
306pub async fn clear_token(instance_id: &str, token_type: &str) -> Result<(), StorageError> {
337 let token_path = format!("runbeam/{}.json", token_type);
338 tracing::debug!(
339 "Clearing token: type={}, instance={}, path={}",
340 token_type,
341 instance_id,
342 token_path
343 );
344
345 let backend = get_storage_backend(instance_id, &token_path).await?;
346
347 let exists = match &backend {
349 StorageBackendType::Keyring(storage) => storage.exists_str(&token_path),
350 StorageBackendType::Encrypted(storage) => storage.exists_str(&token_path),
351 };
352
353 if !exists {
354 tracing::debug!("No token file to clear: type={}", token_type);
355 return Ok(());
356 }
357
358 match backend {
360 StorageBackendType::Keyring(storage) => storage.remove_str(&token_path).await?,
361 StorageBackendType::Encrypted(storage) => storage.remove_str(&token_path).await?,
362 }
363
364 tracing::info!("Token cleared successfully: type={}", token_type);
365
366 Ok(())
367}
368
369pub async fn save_token_with_key(
390 instance_id: &str,
391 token: &MachineToken,
392 encryption_key: &str,
393) -> Result<(), StorageError> {
394 let token_path = "runbeam/auth.json";
395 tracing::debug!(
396 "Saving machine token with explicit encryption key: gateway={}, instance={}",
397 token.gateway_code,
398 instance_id
399 );
400
401 let keyring_disabled = std::env::var("RUNBEAM_DISABLE_KEYRING").is_ok();
403
404 let backend = if keyring_disabled {
406 tracing::debug!("Keyring disabled via RUNBEAM_DISABLE_KEYRING environment variable");
407 None
408 } else {
409 let keyring = KeyringStorage::new("runbeam");
410 let path = token_path.to_string();
411 let test_result = tokio::task::spawn_blocking(move || keyring.exists_str(&path)).await;
412
413 match test_result {
414 Ok(_) => {
415 tracing::debug!("Using OS keyring for secure token storage");
416 Some(StorageBackendType::Keyring(KeyringStorage::new("runbeam")))
417 }
418 Err(e) => {
419 tracing::debug!(
420 "Keyring unavailable ({}), using encrypted filesystem with provided key",
421 e
422 );
423 None
424 }
425 }
426 };
427
428 let backend = match backend {
429 Some(b) => b,
430 None => {
431 let encrypted =
433 EncryptedFilesystemStorage::new_with_instance_and_key(instance_id, encryption_key)
434 .await?;
435 StorageBackendType::Encrypted(encrypted)
436 }
437 };
438
439 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
441 tracing::error!("Failed to serialize machine token: {}", e);
442 StorageError::Config(format!("JSON serialization failed: {}", e))
443 })?;
444
445 match backend {
447 StorageBackendType::Keyring(storage) => storage.write_file_str(token_path, &json).await?,
448 StorageBackendType::Encrypted(storage) => storage.write_file_str(token_path, &json).await?,
449 }
450
451 tracing::info!(
452 "Machine token saved successfully with explicit key: gateway_id={}, expires_at={}",
453 token.gateway_id,
454 token.expires_at
455 );
456
457 Ok(())
458}
459
460pub async fn save_machine_token(
486 instance_id: &str,
487 token: &MachineToken,
488) -> Result<(), StorageError> {
489 save_token(instance_id, "auth", token).await
490}
491
492pub async fn load_machine_token(instance_id: &str) -> Result<Option<MachineToken>, StorageError> {
512 load_token(instance_id, "auth").await
513}
514
515pub async fn clear_machine_token(instance_id: &str) -> Result<(), StorageError> {
534 clear_token(instance_id, "auth").await
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use serial_test::serial;
541
542 fn setup_test_encryption() -> impl Drop {
544 use base64::Engine;
545 use secrecy::ExposeSecret;
546 use std::env;
547
548 let identity = age::x25519::Identity::generate();
549 let key_base64 = base64::engine::general_purpose::STANDARD
550 .encode(identity.to_string().expose_secret().as_bytes());
551 env::set_var("RUNBEAM_ENCRYPTION_KEY", &key_base64);
552
553 env::set_var("RUNBEAM_DISABLE_KEYRING", "1");
556
557 struct Guard;
559 impl Drop for Guard {
560 fn drop(&mut self) {
561 std::env::remove_var("RUNBEAM_ENCRYPTION_KEY");
562 std::env::remove_var("RUNBEAM_DISABLE_KEYRING");
563 }
564 }
565 Guard
566 }
567
568 #[test]
569 fn test_machine_token_creation() {
570 let token = MachineToken::new(
571 "test_token".to_string(),
572 "2025-12-31T23:59:59Z".to_string(),
573 "gw123".to_string(),
574 "gateway-code-123".to_string(),
575 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
576 );
577
578 assert_eq!(token.machine_token, "test_token");
579 assert_eq!(token.gateway_id, "gw123");
580 assert_eq!(token.gateway_code, "gateway-code-123");
581 assert_eq!(token.abilities.len(), 2);
582 assert!(!token.issued_at.is_empty());
583 }
584
585 #[test]
586 fn test_machine_token_is_expired() {
587 let expired_token = MachineToken::new(
589 "test_token".to_string(),
590 "2020-01-01T00:00:00Z".to_string(),
591 "gw123".to_string(),
592 "gateway-code-123".to_string(),
593 vec![],
594 );
595 assert!(expired_token.is_expired());
596 assert!(!expired_token.is_valid());
597
598 let valid_token = MachineToken::new(
600 "test_token".to_string(),
601 "2099-12-31T23:59:59Z".to_string(),
602 "gw123".to_string(),
603 "gateway-code-123".to_string(),
604 vec![],
605 );
606 assert!(!valid_token.is_expired());
607 assert!(valid_token.is_valid());
608 }
609
610 #[test]
611 fn test_machine_token_serialization() {
612 let token = MachineToken::new(
613 "test_token".to_string(),
614 "2025-12-31T23:59:59Z".to_string(),
615 "gw123".to_string(),
616 "gateway-code-123".to_string(),
617 vec!["harmony:send".to_string()],
618 );
619
620 let json = serde_json::to_string(&token).unwrap();
621 assert!(json.contains("\"machine_token\":\"test_token\""));
622 assert!(json.contains("\"gateway_id\":\"gw123\""));
623 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
624
625 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
627 assert_eq!(deserialized.machine_token, token.machine_token);
628 assert_eq!(deserialized.gateway_id, token.gateway_id);
629 }
630
631 #[tokio::test]
632 #[serial]
633 async fn test_save_and_load_token_secure() {
634 let _guard = setup_test_encryption();
635 let instance_id = "test-save-load";
636 let _ = clear_machine_token(instance_id).await;
638
639 let token = MachineToken::new(
640 "test_token_secure".to_string(),
641 "2099-12-31T23:59:59Z".to_string(),
642 "gw_test".to_string(),
643 "test-gateway".to_string(),
644 vec!["harmony:send".to_string()],
645 );
646
647 save_machine_token(instance_id, &token).await.unwrap();
649
650 let loaded = load_machine_token(instance_id).await.unwrap();
652 assert!(loaded.is_some());
653
654 let loaded_token = loaded.unwrap();
655 assert_eq!(loaded_token.machine_token, token.machine_token);
656 assert_eq!(loaded_token.gateway_id, token.gateway_id);
657 assert_eq!(loaded_token.gateway_code, token.gateway_code);
658 assert!(loaded_token.is_valid());
659
660 clear_machine_token(instance_id).await.unwrap();
662 }
663
664 #[tokio::test]
665 #[serial]
666 async fn test_load_nonexistent_token_secure() {
667 let _guard = setup_test_encryption();
668 let instance_id = "test-nonexistent";
669 let _ = clear_machine_token(instance_id).await;
671
672 let result = load_machine_token(instance_id).await.unwrap();
674 assert!(result.is_none());
675 }
676
677 #[tokio::test]
678 #[serial]
679 async fn test_clear_token_secure() {
680 let _guard = setup_test_encryption();
681 let instance_id = "test-clear";
682 let _ = clear_machine_token(instance_id).await;
684
685 let token = MachineToken::new(
686 "test_clear".to_string(),
687 "2099-12-31T23:59:59Z".to_string(),
688 "gw_clear".to_string(),
689 "clear-test".to_string(),
690 vec![],
691 );
692
693 save_machine_token(instance_id, &token).await.unwrap();
695
696 assert!(load_machine_token(instance_id).await.unwrap().is_some());
698
699 clear_machine_token(instance_id).await.unwrap();
701
702 assert!(load_machine_token(instance_id).await.unwrap().is_none());
704 }
705
706 #[tokio::test]
707 #[serial]
708 async fn test_clear_nonexistent_token_secure() {
709 let _guard = setup_test_encryption();
710 let instance_id = "test-clear-nonexistent";
711 clear_machine_token(instance_id).await.unwrap();
713 }
714
715 #[tokio::test]
716 #[serial]
717 async fn test_token_expiry_detection() {
718 let _guard = setup_test_encryption();
719 let instance_id = "test-expiry";
720 let _ = clear_machine_token(instance_id).await;
721
722 let expired_token = MachineToken::new(
724 "expired_token".to_string(),
725 "2020-01-01T00:00:00Z".to_string(),
726 "gw_expired".to_string(),
727 "expired-gateway".to_string(),
728 vec![],
729 );
730
731 save_machine_token(instance_id, &expired_token)
732 .await
733 .unwrap();
734
735 let loaded = load_machine_token(instance_id).await.unwrap();
737 assert!(loaded.is_some());
738 let loaded_token = loaded.unwrap();
739 assert!(loaded_token.is_expired());
740 assert!(!loaded_token.is_valid());
741
742 clear_machine_token(instance_id).await.unwrap();
744 }
745
746 #[tokio::test]
747 #[serial]
748 async fn test_token_with_abilities() {
749 let _guard = setup_test_encryption();
750 let instance_id = "test-abilities";
751 let _ = clear_machine_token(instance_id).await;
752
753 let token = MachineToken::new(
754 "token_with_abilities".to_string(),
755 "2099-12-31T23:59:59Z".to_string(),
756 "gw_abilities".to_string(),
757 "abilities-test".to_string(),
758 vec![
759 "harmony:send".to_string(),
760 "harmony:receive".to_string(),
761 "harmony:config".to_string(),
762 ],
763 );
764
765 save_machine_token(instance_id, &token).await.unwrap();
766
767 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
768 assert_eq!(loaded.abilities.len(), 3);
769 assert!(loaded.abilities.contains(&"harmony:send".to_string()));
770 assert!(loaded.abilities.contains(&"harmony:receive".to_string()));
771 assert!(loaded.abilities.contains(&"harmony:config".to_string()));
772
773 clear_machine_token(instance_id).await.unwrap();
775 }
776
777 #[tokio::test]
778 #[serial]
779 async fn test_token_overwrites_existing() {
780 let _guard = setup_test_encryption();
781 let instance_id = "test-overwrite";
782 let _ = clear_machine_token(instance_id).await;
783
784 let token1 = MachineToken::new(
786 "first_token".to_string(),
787 "2099-12-31T23:59:59Z".to_string(),
788 "gw_first".to_string(),
789 "first-gateway".to_string(),
790 vec![],
791 );
792 save_machine_token(instance_id, &token1).await.unwrap();
793
794 let token2 = MachineToken::new(
796 "second_token".to_string(),
797 "2099-12-31T23:59:59Z".to_string(),
798 "gw_second".to_string(),
799 "second-gateway".to_string(),
800 vec![],
801 );
802 save_machine_token(instance_id, &token2).await.unwrap();
803
804 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
806 assert_eq!(loaded.machine_token, "second_token");
807 assert_eq!(loaded.gateway_id, "gw_second");
808 assert_eq!(loaded.gateway_code, "second-gateway");
809
810 clear_machine_token(instance_id).await.unwrap();
812 }
813
814 #[tokio::test]
815 async fn test_token_encrypted_on_disk() {
816 use crate::storage::EncryptedFilesystemStorage;
817 use tempfile::TempDir;
818
819 let instance_id = "test-encryption-verify";
820 let temp_dir = TempDir::new().unwrap();
821
822 let token = MachineToken::new(
824 "super_secret_token_12345".to_string(),
825 "2099-12-31T23:59:59Z".to_string(),
826 "gw_secret".to_string(),
827 "secret-gateway".to_string(),
828 vec!["harmony:admin".to_string()],
829 );
830
831 let storage_path = temp_dir.path().join(instance_id);
833 let storage = EncryptedFilesystemStorage::new(&storage_path)
834 .await
835 .unwrap();
836
837 let token_json = serde_json::to_vec(&token).unwrap();
839 storage
840 .write_file_str("auth.json", &token_json)
841 .await
842 .unwrap();
843
844 let token_path = storage_path.join("auth.json");
846
847 assert!(
849 token_path.exists(),
850 "Token file should exist at {:?}",
851 token_path
852 );
853
854 let raw_contents = std::fs::read(&token_path).unwrap();
856 let raw_string = String::from_utf8_lossy(&raw_contents);
857
858 assert!(
860 !raw_string.contains("super_secret_token_12345"),
861 "Token file should NOT contain plaintext token: {}",
862 raw_string
863 );
864 assert!(
865 !raw_string.contains("gw_secret"),
866 "Token file should NOT contain plaintext gateway_id: {}",
867 raw_string
868 );
869 assert!(
870 !raw_string.contains("secret-gateway"),
871 "Token file should NOT contain plaintext gateway_code: {}",
872 raw_string
873 );
874 assert!(
875 !raw_string.contains("harmony:admin"),
876 "Token file should NOT contain plaintext abilities: {}",
877 raw_string
878 );
879
880 if raw_contents.len() > 50 {
882 let has_age_header = raw_string.starts_with("age-encryption.org/v1");
884 assert!(
885 has_age_header || raw_contents.starts_with(b"age-encryption.org/v1"),
886 "File should contain age encryption header. Raw contents (first 100 bytes): {:?}",
887 &raw_contents[..std::cmp::min(100, raw_contents.len())]
888 );
889 }
890
891 let decrypted_data = storage.read_file_str("auth.json").await.unwrap();
893 let loaded_token: MachineToken = serde_json::from_slice(&decrypted_data).unwrap();
894 assert_eq!(loaded_token.machine_token, "super_secret_token_12345");
895 assert_eq!(loaded_token.gateway_id, "gw_secret");
896 }
897
898 #[tokio::test]
899 async fn test_token_file_cannot_be_read_as_json() {
900 use crate::storage::EncryptedFilesystemStorage;
901 use tempfile::TempDir;
902
903 let instance_id = "test-raw-json-read";
904 let temp_dir = TempDir::new().unwrap();
905 let storage_path = temp_dir.path().join(instance_id);
906
907 let token = MachineToken::new(
908 "test_token_json".to_string(),
909 "2099-12-31T23:59:59Z".to_string(),
910 "gw_json".to_string(),
911 "json-test".to_string(),
912 vec![],
913 );
914
915 let storage = EncryptedFilesystemStorage::new(&storage_path)
917 .await
918 .unwrap();
919 let token_json = serde_json::to_vec(&token).unwrap();
920 storage
921 .write_file_str("auth.json", &token_json)
922 .await
923 .unwrap();
924
925 let token_path = storage_path.join("auth.json");
927
928 let raw_contents = std::fs::read(&token_path).unwrap();
930
931 let json_parse_result: Result<serde_json::Value, _> = serde_json::from_slice(&raw_contents);
933 assert!(
934 json_parse_result.is_err(),
935 "Raw token file should NOT be parseable as JSON (it should be encrypted)"
936 );
937 }
938
939 #[tokio::test]
940 async fn test_token_different_from_plaintext() {
941 use crate::storage::EncryptedFilesystemStorage;
942 use tempfile::TempDir;
943
944 let instance_id = "test-plaintext-compare";
945 let temp_dir = TempDir::new().unwrap();
946 let storage_path = temp_dir.path().join(instance_id);
947
948 let token = MachineToken::new(
949 "comparison_token".to_string(),
950 "2099-12-31T23:59:59Z".to_string(),
951 "gw_compare".to_string(),
952 "compare-gateway".to_string(),
953 vec!["test:ability".to_string()],
954 );
955
956 let plaintext_json = serde_json::to_vec(&token).unwrap();
958
959 let storage = EncryptedFilesystemStorage::new(&storage_path)
961 .await
962 .unwrap();
963 storage
964 .write_file_str("auth.json", &plaintext_json)
965 .await
966 .unwrap();
967
968 let token_path = storage_path.join("auth.json");
970 let encrypted_contents = std::fs::read(&token_path).unwrap();
971
972 assert_ne!(
974 encrypted_contents, plaintext_json,
975 "Encrypted file contents should differ from plaintext JSON"
976 );
977
978 assert!(
980 encrypted_contents.len() > plaintext_json.len(),
981 "Encrypted file should be larger due to encryption overhead. Encrypted: {}, Plaintext: {}",
982 encrypted_contents.len(),
983 plaintext_json.len()
984 );
985 }
986
987 #[tokio::test]
988 async fn test_multiple_instances_isolated() {
989 use crate::storage::EncryptedFilesystemStorage;
990 use tempfile::TempDir;
991
992 let temp_dir = TempDir::new().unwrap();
993
994 let token1 = MachineToken::new(
996 "token_instance_1".to_string(),
997 "2099-12-31T23:59:59Z".to_string(),
998 "gw_1".to_string(),
999 "gateway-1".to_string(),
1000 vec![],
1001 );
1002
1003 let token2 = MachineToken::new(
1004 "token_instance_2".to_string(),
1005 "2099-12-31T23:59:59Z".to_string(),
1006 "gw_2".to_string(),
1007 "gateway-2".to_string(),
1008 vec![],
1009 );
1010
1011 let storage1_path = temp_dir.path().join("instance-1");
1013 let storage2_path = temp_dir.path().join("instance-2");
1014
1015 let storage1 = EncryptedFilesystemStorage::new(&storage1_path)
1016 .await
1017 .unwrap();
1018 let storage2 = EncryptedFilesystemStorage::new(&storage2_path)
1019 .await
1020 .unwrap();
1021
1022 let token1_json = serde_json::to_vec(&token1).unwrap();
1024 let token2_json = serde_json::to_vec(&token2).unwrap();
1025 storage1
1026 .write_file_str("auth.json", &token1_json)
1027 .await
1028 .unwrap();
1029 storage2
1030 .write_file_str("auth.json", &token2_json)
1031 .await
1032 .unwrap();
1033
1034 let path1 = storage1_path.join("auth.json");
1036 let path2 = storage2_path.join("auth.json");
1037
1038 assert!(path1.exists(), "Instance 1 token file should exist");
1039 assert!(path2.exists(), "Instance 2 token file should exist");
1040 assert_ne!(path1, path2, "Token files should be in different locations");
1041
1042 let key1_path = storage1_path.join("encryption.key");
1044 let key2_path = storage2_path.join("encryption.key");
1045
1046 if key1_path.exists() && key2_path.exists() {
1047 let key1_contents = std::fs::read(&key1_path).unwrap();
1048 let key2_contents = std::fs::read(&key2_path).unwrap();
1049 assert_ne!(
1050 key1_contents, key2_contents,
1051 "Encryption keys should be different for each instance"
1052 );
1053 }
1054
1055 let decrypted1 = storage1.read_file_str("auth.json").await.unwrap();
1057 let decrypted2 = storage2.read_file_str("auth.json").await.unwrap();
1058
1059 let loaded1: MachineToken = serde_json::from_slice(&decrypted1).unwrap();
1060 let loaded2: MachineToken = serde_json::from_slice(&decrypted2).unwrap();
1061
1062 assert_eq!(loaded1.machine_token, "token_instance_1");
1063 assert_eq!(loaded1.gateway_code, "gateway-1");
1064 assert_eq!(loaded2.machine_token, "token_instance_2");
1065 assert_eq!(loaded2.gateway_code, "gateway-2");
1066 }
1067
1068 #[tokio::test]
1069 #[cfg(unix)]
1070 async fn test_encryption_key_file_permissions() {
1071 use crate::storage::EncryptedFilesystemStorage;
1072 use std::os::unix::fs::PermissionsExt;
1073 use tempfile::TempDir;
1074
1075 let instance_id = "test-key-permissions";
1076 let temp_dir = TempDir::new().unwrap();
1077 let storage_path = temp_dir.path().join(instance_id);
1078
1079 let _storage = EncryptedFilesystemStorage::new(&storage_path)
1081 .await
1082 .unwrap();
1083
1084 let key_path = storage_path.join("encryption.key");
1086
1087 if !key_path.exists() {
1089 return;
1091 }
1092
1093 let metadata = std::fs::metadata(&key_path).unwrap();
1094 let permissions = metadata.permissions();
1095 let mode = permissions.mode();
1096
1097 let permission_bits = mode & 0o777;
1099 assert_eq!(
1100 permission_bits, 0o600,
1101 "Encryption key file should have 0600 permissions (owner read/write only), got {:o}",
1102 permission_bits
1103 );
1104 }
1105
1106 #[tokio::test]
1107 async fn test_tampered_token_file_fails_to_load() {
1108 use crate::storage::EncryptedFilesystemStorage;
1109 use tempfile::TempDir;
1110
1111 let instance_id = "test-tamper";
1112 let temp_dir = TempDir::new().unwrap();
1113 let storage_path = temp_dir.path().join(instance_id);
1114
1115 let token = MachineToken::new(
1116 "original_token".to_string(),
1117 "2099-12-31T23:59:59Z".to_string(),
1118 "gw_tamper".to_string(),
1119 "tamper-test".to_string(),
1120 vec![],
1121 );
1122
1123 let storage = EncryptedFilesystemStorage::new(&storage_path)
1125 .await
1126 .unwrap();
1127 let token_json = serde_json::to_vec(&token).unwrap();
1128 storage
1129 .write_file_str("auth.json", &token_json)
1130 .await
1131 .unwrap();
1132
1133 let token_path = storage_path.join("auth.json");
1135 let mut contents = std::fs::read(&token_path).unwrap();
1136
1137 if contents.len() > 50 {
1139 contents[25] = contents[25].wrapping_add(1);
1140 contents[30] = contents[30].wrapping_sub(1);
1141 std::fs::write(&token_path, contents).unwrap();
1142 }
1143
1144 let result = storage.read_file_str("auth.json").await;
1146 assert!(
1147 result.is_err(),
1148 "Loading tampered encrypted file should fail"
1149 );
1150 }
1151
1152 #[tokio::test]
1157 #[serial]
1158 async fn test_generic_save_and_load_user_token() {
1159 use crate::runbeam_api::types::UserToken;
1160 let _guard = setup_test_encryption();
1161 let instance_id = "test-user-token";
1162 clear_token(instance_id, "user_auth").await.ok();
1163
1164 let user_token = UserToken::new(
1165 "user_jwt_token".to_string(),
1166 Some(1234567890),
1167 Some(crate::runbeam_api::types::UserInfo {
1168 id: "user123".to_string(),
1169 name: "Test User".to_string(),
1170 email: "test@example.com".to_string(),
1171 }),
1172 );
1173
1174 save_token(instance_id, "user_auth", &user_token)
1176 .await
1177 .unwrap();
1178
1179 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1181 assert!(loaded.is_some());
1182
1183 let loaded_token = loaded.unwrap();
1184 assert_eq!(loaded_token.token, user_token.token);
1185 assert_eq!(loaded_token.expires_at, user_token.expires_at);
1186 assert!(loaded_token.user.is_some());
1187
1188 clear_token(instance_id, "user_auth").await.unwrap();
1190 }
1191
1192 #[tokio::test]
1193 #[serial]
1194 async fn test_different_token_types_isolated() {
1195 use crate::runbeam_api::types::UserToken;
1196 let _guard = setup_test_encryption();
1197 let instance_id = "test-isolation";
1198
1199 let user_token = UserToken::new("user_token".to_string(), None, None);
1201
1202 let machine_token = MachineToken::new(
1203 "machine_token".to_string(),
1204 "2099-12-31T23:59:59Z".to_string(),
1205 "gw_test".to_string(),
1206 "test-gw".to_string(),
1207 vec![],
1208 );
1209
1210 save_token(instance_id, "user_auth", &user_token)
1212 .await
1213 .unwrap();
1214 save_token(instance_id, "auth", &machine_token)
1215 .await
1216 .unwrap();
1217
1218 let loaded_user: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1220 let loaded_machine: Option<MachineToken> = load_token(instance_id, "auth").await.unwrap();
1221
1222 assert!(loaded_user.is_some());
1223 assert!(loaded_machine.is_some());
1224 assert_eq!(loaded_user.unwrap().token, "user_token");
1225 assert_eq!(loaded_machine.unwrap().machine_token, "machine_token");
1226
1227 clear_token(instance_id, "user_auth").await.unwrap();
1229 clear_token(instance_id, "auth").await.unwrap();
1230 }
1231
1232 #[tokio::test]
1233 #[serial]
1234 async fn test_user_token_with_full_metadata() {
1235 use crate::runbeam_api::types::UserToken;
1236 let _guard = setup_test_encryption();
1237 let instance_id = "test-user-full";
1238 clear_token(instance_id, "user_auth").await.ok();
1239
1240 let user_token = UserToken::new(
1241 "detailed_user_token".to_string(),
1242 Some(2000000000),
1243 Some(crate::runbeam_api::types::UserInfo {
1244 id: "user456".to_string(),
1245 name: "John Doe".to_string(),
1246 email: "john@example.com".to_string(),
1247 }),
1248 );
1249
1250 save_token(instance_id, "user_auth", &user_token)
1252 .await
1253 .unwrap();
1254 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1255
1256 assert!(loaded.is_some());
1257 let loaded_token = loaded.unwrap();
1258 assert_eq!(loaded_token.token, "detailed_user_token");
1259 assert_eq!(loaded_token.expires_at, Some(2000000000));
1260
1261 let user_info = loaded_token.user.unwrap();
1262 assert_eq!(user_info.id, "user456");
1263 assert_eq!(user_info.name, "John Doe");
1264 assert_eq!(user_info.email, "john@example.com");
1265
1266 clear_token(instance_id, "user_auth").await.unwrap();
1268 }
1269}