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>
268where
269 T: DeserializeOwned,
270{
271 let token_path = format!("runbeam/{}.json", token_type);
272 tracing::debug!(
273 "Loading token: type={}, instance={}, path={}",
274 token_type,
275 instance_id,
276 token_path
277 );
278
279 let keyring_disabled = std::env::var("RUNBEAM_DISABLE_KEYRING").is_ok();
281
282 if !keyring_disabled {
284 tracing::debug!("Attempting to load token from keyring storage");
285 let keyring = KeyringStorage::new("runbeam");
286
287 if keyring.exists_str(&token_path) {
288 tracing::debug!("Token found in keyring, loading...");
289 match keyring.read_file_str(&token_path).await {
290 Ok(json) => {
291 let token: T = serde_json::from_slice(&json).map_err(|e| {
293 tracing::error!("Failed to deserialize token from keyring: {}", e);
294 StorageError::Config(format!("JSON deserialization failed: {}", e))
295 })?;
296
297 tracing::debug!(
298 "Token loaded successfully from keyring: type={}",
299 token_type
300 );
301 return Ok(Some(token));
302 }
303 Err(e) => {
304 tracing::warn!("Token exists in keyring but failed to read: {}", e);
305 }
307 }
308 } else {
309 tracing::debug!("Token not found in keyring, trying encrypted filesystem");
310 }
311 } else {
312 tracing::debug!("Keyring disabled, skipping keyring check");
313 }
314
315 tracing::debug!("Attempting to load token from encrypted filesystem storage");
317 let encrypted = EncryptedFilesystemStorage::new_with_instance(instance_id)
318 .await
319 .map_err(|e| {
320 tracing::debug!("Failed to initialize encrypted storage: {}", e);
321 e
322 })?;
323
324 if encrypted.exists_str(&token_path) {
325 tracing::debug!("Token found in encrypted filesystem, loading...");
326 let json = encrypted.read_file_str(&token_path).await?;
327
328 let token: T = serde_json::from_slice(&json).map_err(|e| {
330 tracing::error!(
331 "Failed to deserialize token from encrypted filesystem: {}",
332 e
333 );
334 StorageError::Config(format!("JSON deserialization failed: {}", e))
335 })?;
336
337 tracing::debug!(
338 "Token loaded successfully from encrypted filesystem: type={}",
339 token_type
340 );
341 return Ok(Some(token));
342 }
343
344 tracing::debug!(
345 "No token file found in any storage backend: type={}",
346 token_type
347 );
348 Ok(None)
349}
350
351pub async fn clear_token(instance_id: &str, token_type: &str) -> Result<(), StorageError> {
384 let token_path = format!("runbeam/{}.json", token_type);
385 tracing::debug!(
386 "Clearing token: type={}, instance={}, path={}",
387 token_type,
388 instance_id,
389 token_path
390 );
391
392 let mut cleared_any = false;
393
394 let keyring_disabled = std::env::var("RUNBEAM_DISABLE_KEYRING").is_ok();
396
397 if !keyring_disabled {
399 let keyring = KeyringStorage::new("runbeam");
400 if keyring.exists_str(&token_path) {
401 tracing::debug!("Clearing token from keyring storage");
402 match keyring.remove_str(&token_path).await {
403 Ok(_) => {
404 tracing::debug!("Token cleared from keyring");
405 cleared_any = true;
406 }
407 Err(e) => {
408 tracing::warn!("Failed to clear token from keyring: {}", e);
409 }
411 }
412 }
413 }
414
415 if let Ok(encrypted) = EncryptedFilesystemStorage::new_with_instance(instance_id).await {
417 if encrypted.exists_str(&token_path) {
418 tracing::debug!("Clearing token from encrypted filesystem storage");
419 match encrypted.remove_str(&token_path).await {
420 Ok(_) => {
421 tracing::debug!("Token cleared from encrypted filesystem");
422 cleared_any = true;
423 }
424 Err(e) => {
425 tracing::warn!("Failed to clear token from encrypted filesystem: {}", e);
426 }
427 }
428 }
429 }
430
431 if cleared_any {
432 tracing::info!("Token cleared successfully: type={}", token_type);
433 } else {
434 tracing::debug!(
435 "No token file to clear in any storage backend: type={}",
436 token_type
437 );
438 }
439
440 Ok(())
441}
442
443pub async fn save_token_with_key(
464 instance_id: &str,
465 token: &MachineToken,
466 encryption_key: &str,
467) -> Result<(), StorageError> {
468 let token_path = "runbeam/auth.json";
469 tracing::debug!(
470 "Saving machine token with explicit encryption key: gateway={}, instance={}",
471 token.gateway_code,
472 instance_id
473 );
474
475 let keyring_disabled = std::env::var("RUNBEAM_DISABLE_KEYRING").is_ok();
477
478 let backend = if keyring_disabled {
480 tracing::debug!("Keyring disabled via RUNBEAM_DISABLE_KEYRING environment variable");
481 None
482 } else {
483 let keyring = KeyringStorage::new("runbeam");
484 let path = token_path.to_string();
485 let test_result = tokio::task::spawn_blocking(move || keyring.exists_str(&path)).await;
486
487 match test_result {
488 Ok(_) => {
489 tracing::debug!("Using OS keyring for secure token storage");
490 Some(StorageBackendType::Keyring(KeyringStorage::new("runbeam")))
491 }
492 Err(e) => {
493 tracing::debug!(
494 "Keyring unavailable ({}), using encrypted filesystem with provided key",
495 e
496 );
497 None
498 }
499 }
500 };
501
502 let backend = match backend {
503 Some(b) => b,
504 None => {
505 let encrypted =
507 EncryptedFilesystemStorage::new_with_instance_and_key(instance_id, encryption_key)
508 .await?;
509 StorageBackendType::Encrypted(encrypted)
510 }
511 };
512
513 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
515 tracing::error!("Failed to serialize machine token: {}", e);
516 StorageError::Config(format!("JSON serialization failed: {}", e))
517 })?;
518
519 match backend {
521 StorageBackendType::Keyring(storage) => storage.write_file_str(token_path, &json).await?,
522 StorageBackendType::Encrypted(storage) => storage.write_file_str(token_path, &json).await?,
523 }
524
525 tracing::info!(
526 "Machine token saved successfully with explicit key: gateway_id={}, expires_at={}",
527 token.gateway_id,
528 token.expires_at
529 );
530
531 Ok(())
532}
533
534pub async fn save_machine_token(
560 instance_id: &str,
561 token: &MachineToken,
562) -> Result<(), StorageError> {
563 save_token(instance_id, "auth", token).await
564}
565
566pub async fn load_machine_token(instance_id: &str) -> Result<Option<MachineToken>, StorageError> {
586 load_token(instance_id, "auth").await
587}
588
589pub async fn clear_machine_token(instance_id: &str) -> Result<(), StorageError> {
608 clear_token(instance_id, "auth").await
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use serial_test::serial;
615
616 fn setup_test_encryption() -> impl Drop {
618 use base64::Engine;
619 use secrecy::ExposeSecret;
620 use std::env;
621
622 let identity = age::x25519::Identity::generate();
623 let key_base64 = base64::engine::general_purpose::STANDARD
624 .encode(identity.to_string().expose_secret().as_bytes());
625 env::set_var("RUNBEAM_ENCRYPTION_KEY", &key_base64);
626
627 env::set_var("RUNBEAM_DISABLE_KEYRING", "1");
630
631 struct Guard;
633 impl Drop for Guard {
634 fn drop(&mut self) {
635 std::env::remove_var("RUNBEAM_ENCRYPTION_KEY");
636 std::env::remove_var("RUNBEAM_DISABLE_KEYRING");
637 }
638 }
639 Guard
640 }
641
642 #[test]
643 fn test_machine_token_creation() {
644 let token = MachineToken::new(
645 "test_token".to_string(),
646 "2025-12-31T23:59:59Z".to_string(),
647 "gw123".to_string(),
648 "gateway-code-123".to_string(),
649 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
650 );
651
652 assert_eq!(token.machine_token, "test_token");
653 assert_eq!(token.gateway_id, "gw123");
654 assert_eq!(token.gateway_code, "gateway-code-123");
655 assert_eq!(token.abilities.len(), 2);
656 assert!(!token.issued_at.is_empty());
657 }
658
659 #[test]
660 fn test_machine_token_is_expired() {
661 let expired_token = MachineToken::new(
663 "test_token".to_string(),
664 "2020-01-01T00:00:00Z".to_string(),
665 "gw123".to_string(),
666 "gateway-code-123".to_string(),
667 vec![],
668 );
669 assert!(expired_token.is_expired());
670 assert!(!expired_token.is_valid());
671
672 let valid_token = MachineToken::new(
674 "test_token".to_string(),
675 "2099-12-31T23:59:59Z".to_string(),
676 "gw123".to_string(),
677 "gateway-code-123".to_string(),
678 vec![],
679 );
680 assert!(!valid_token.is_expired());
681 assert!(valid_token.is_valid());
682 }
683
684 #[test]
685 fn test_machine_token_serialization() {
686 let token = MachineToken::new(
687 "test_token".to_string(),
688 "2025-12-31T23:59:59Z".to_string(),
689 "gw123".to_string(),
690 "gateway-code-123".to_string(),
691 vec!["harmony:send".to_string()],
692 );
693
694 let json = serde_json::to_string(&token).unwrap();
695 assert!(json.contains("\"machine_token\":\"test_token\""));
696 assert!(json.contains("\"gateway_id\":\"gw123\""));
697 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
698
699 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
701 assert_eq!(deserialized.machine_token, token.machine_token);
702 assert_eq!(deserialized.gateway_id, token.gateway_id);
703 }
704
705 #[tokio::test]
706 #[serial]
707 async fn test_save_and_load_token_secure() {
708 let _guard = setup_test_encryption();
709 let instance_id = "test-save-load";
710 let _ = clear_machine_token(instance_id).await;
712
713 let token = MachineToken::new(
714 "test_token_secure".to_string(),
715 "2099-12-31T23:59:59Z".to_string(),
716 "gw_test".to_string(),
717 "test-gateway".to_string(),
718 vec!["harmony:send".to_string()],
719 );
720
721 save_machine_token(instance_id, &token).await.unwrap();
723
724 let loaded = load_machine_token(instance_id).await.unwrap();
726 assert!(loaded.is_some());
727
728 let loaded_token = loaded.unwrap();
729 assert_eq!(loaded_token.machine_token, token.machine_token);
730 assert_eq!(loaded_token.gateway_id, token.gateway_id);
731 assert_eq!(loaded_token.gateway_code, token.gateway_code);
732 assert!(loaded_token.is_valid());
733
734 clear_machine_token(instance_id).await.unwrap();
736 }
737
738 #[tokio::test]
739 #[serial]
740 async fn test_load_nonexistent_token_secure() {
741 let _guard = setup_test_encryption();
742 let instance_id = "test-nonexistent";
743 let _ = clear_machine_token(instance_id).await;
745
746 let result = load_machine_token(instance_id).await.unwrap();
748 assert!(result.is_none());
749 }
750
751 #[tokio::test]
752 #[serial]
753 async fn test_clear_token_secure() {
754 let _guard = setup_test_encryption();
755 let instance_id = "test-clear";
756 let _ = clear_machine_token(instance_id).await;
758
759 let token = MachineToken::new(
760 "test_clear".to_string(),
761 "2099-12-31T23:59:59Z".to_string(),
762 "gw_clear".to_string(),
763 "clear-test".to_string(),
764 vec![],
765 );
766
767 save_machine_token(instance_id, &token).await.unwrap();
769
770 assert!(load_machine_token(instance_id).await.unwrap().is_some());
772
773 clear_machine_token(instance_id).await.unwrap();
775
776 assert!(load_machine_token(instance_id).await.unwrap().is_none());
778 }
779
780 #[tokio::test]
781 #[serial]
782 async fn test_clear_nonexistent_token_secure() {
783 let _guard = setup_test_encryption();
784 let instance_id = "test-clear-nonexistent";
785 clear_machine_token(instance_id).await.unwrap();
787 }
788
789 #[tokio::test]
790 #[serial]
791 async fn test_token_expiry_detection() {
792 let _guard = setup_test_encryption();
793 let instance_id = "test-expiry";
794 let _ = clear_machine_token(instance_id).await;
795
796 let expired_token = MachineToken::new(
798 "expired_token".to_string(),
799 "2020-01-01T00:00:00Z".to_string(),
800 "gw_expired".to_string(),
801 "expired-gateway".to_string(),
802 vec![],
803 );
804
805 save_machine_token(instance_id, &expired_token)
806 .await
807 .unwrap();
808
809 let loaded = load_machine_token(instance_id).await.unwrap();
811 assert!(loaded.is_some());
812 let loaded_token = loaded.unwrap();
813 assert!(loaded_token.is_expired());
814 assert!(!loaded_token.is_valid());
815
816 clear_machine_token(instance_id).await.unwrap();
818 }
819
820 #[tokio::test]
821 #[serial]
822 async fn test_token_with_abilities() {
823 let _guard = setup_test_encryption();
824 let instance_id = "test-abilities";
825 let _ = clear_machine_token(instance_id).await;
826
827 let token = MachineToken::new(
828 "token_with_abilities".to_string(),
829 "2099-12-31T23:59:59Z".to_string(),
830 "gw_abilities".to_string(),
831 "abilities-test".to_string(),
832 vec![
833 "harmony:send".to_string(),
834 "harmony:receive".to_string(),
835 "harmony:config".to_string(),
836 ],
837 );
838
839 save_machine_token(instance_id, &token).await.unwrap();
840
841 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
842 assert_eq!(loaded.abilities.len(), 3);
843 assert!(loaded.abilities.contains(&"harmony:send".to_string()));
844 assert!(loaded.abilities.contains(&"harmony:receive".to_string()));
845 assert!(loaded.abilities.contains(&"harmony:config".to_string()));
846
847 clear_machine_token(instance_id).await.unwrap();
849 }
850
851 #[tokio::test]
852 #[serial]
853 async fn test_token_overwrites_existing() {
854 let _guard = setup_test_encryption();
855 let instance_id = "test-overwrite";
856 let _ = clear_machine_token(instance_id).await;
857
858 let token1 = MachineToken::new(
860 "first_token".to_string(),
861 "2099-12-31T23:59:59Z".to_string(),
862 "gw_first".to_string(),
863 "first-gateway".to_string(),
864 vec![],
865 );
866 save_machine_token(instance_id, &token1).await.unwrap();
867
868 let token2 = MachineToken::new(
870 "second_token".to_string(),
871 "2099-12-31T23:59:59Z".to_string(),
872 "gw_second".to_string(),
873 "second-gateway".to_string(),
874 vec![],
875 );
876 save_machine_token(instance_id, &token2).await.unwrap();
877
878 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
880 assert_eq!(loaded.machine_token, "second_token");
881 assert_eq!(loaded.gateway_id, "gw_second");
882 assert_eq!(loaded.gateway_code, "second-gateway");
883
884 clear_machine_token(instance_id).await.unwrap();
886 }
887
888 #[tokio::test]
889 async fn test_token_encrypted_on_disk() {
890 use crate::storage::EncryptedFilesystemStorage;
891 use tempfile::TempDir;
892
893 let instance_id = "test-encryption-verify";
894 let temp_dir = TempDir::new().unwrap();
895
896 let token = MachineToken::new(
898 "super_secret_token_12345".to_string(),
899 "2099-12-31T23:59:59Z".to_string(),
900 "gw_secret".to_string(),
901 "secret-gateway".to_string(),
902 vec!["harmony:admin".to_string()],
903 );
904
905 let storage_path = temp_dir.path().join(instance_id);
907 let storage = EncryptedFilesystemStorage::new(&storage_path)
908 .await
909 .unwrap();
910
911 let token_json = serde_json::to_vec(&token).unwrap();
913 storage
914 .write_file_str("auth.json", &token_json)
915 .await
916 .unwrap();
917
918 let token_path = storage_path.join("auth.json");
920
921 assert!(
923 token_path.exists(),
924 "Token file should exist at {:?}",
925 token_path
926 );
927
928 let raw_contents = std::fs::read(&token_path).unwrap();
930 let raw_string = String::from_utf8_lossy(&raw_contents);
931
932 assert!(
934 !raw_string.contains("super_secret_token_12345"),
935 "Token file should NOT contain plaintext token: {}",
936 raw_string
937 );
938 assert!(
939 !raw_string.contains("gw_secret"),
940 "Token file should NOT contain plaintext gateway_id: {}",
941 raw_string
942 );
943 assert!(
944 !raw_string.contains("secret-gateway"),
945 "Token file should NOT contain plaintext gateway_code: {}",
946 raw_string
947 );
948 assert!(
949 !raw_string.contains("harmony:admin"),
950 "Token file should NOT contain plaintext abilities: {}",
951 raw_string
952 );
953
954 if raw_contents.len() > 50 {
956 let has_age_header = raw_string.starts_with("age-encryption.org/v1");
958 assert!(
959 has_age_header || raw_contents.starts_with(b"age-encryption.org/v1"),
960 "File should contain age encryption header. Raw contents (first 100 bytes): {:?}",
961 &raw_contents[..std::cmp::min(100, raw_contents.len())]
962 );
963 }
964
965 let decrypted_data = storage.read_file_str("auth.json").await.unwrap();
967 let loaded_token: MachineToken = serde_json::from_slice(&decrypted_data).unwrap();
968 assert_eq!(loaded_token.machine_token, "super_secret_token_12345");
969 assert_eq!(loaded_token.gateway_id, "gw_secret");
970 }
971
972 #[tokio::test]
973 async fn test_token_file_cannot_be_read_as_json() {
974 use crate::storage::EncryptedFilesystemStorage;
975 use tempfile::TempDir;
976
977 let instance_id = "test-raw-json-read";
978 let temp_dir = TempDir::new().unwrap();
979 let storage_path = temp_dir.path().join(instance_id);
980
981 let token = MachineToken::new(
982 "test_token_json".to_string(),
983 "2099-12-31T23:59:59Z".to_string(),
984 "gw_json".to_string(),
985 "json-test".to_string(),
986 vec![],
987 );
988
989 let storage = EncryptedFilesystemStorage::new(&storage_path)
991 .await
992 .unwrap();
993 let token_json = serde_json::to_vec(&token).unwrap();
994 storage
995 .write_file_str("auth.json", &token_json)
996 .await
997 .unwrap();
998
999 let token_path = storage_path.join("auth.json");
1001
1002 let raw_contents = std::fs::read(&token_path).unwrap();
1004
1005 let json_parse_result: Result<serde_json::Value, _> = serde_json::from_slice(&raw_contents);
1007 assert!(
1008 json_parse_result.is_err(),
1009 "Raw token file should NOT be parseable as JSON (it should be encrypted)"
1010 );
1011 }
1012
1013 #[tokio::test]
1014 async fn test_token_different_from_plaintext() {
1015 use crate::storage::EncryptedFilesystemStorage;
1016 use tempfile::TempDir;
1017
1018 let instance_id = "test-plaintext-compare";
1019 let temp_dir = TempDir::new().unwrap();
1020 let storage_path = temp_dir.path().join(instance_id);
1021
1022 let token = MachineToken::new(
1023 "comparison_token".to_string(),
1024 "2099-12-31T23:59:59Z".to_string(),
1025 "gw_compare".to_string(),
1026 "compare-gateway".to_string(),
1027 vec!["test:ability".to_string()],
1028 );
1029
1030 let plaintext_json = serde_json::to_vec(&token).unwrap();
1032
1033 let storage = EncryptedFilesystemStorage::new(&storage_path)
1035 .await
1036 .unwrap();
1037 storage
1038 .write_file_str("auth.json", &plaintext_json)
1039 .await
1040 .unwrap();
1041
1042 let token_path = storage_path.join("auth.json");
1044 let encrypted_contents = std::fs::read(&token_path).unwrap();
1045
1046 assert_ne!(
1048 encrypted_contents, plaintext_json,
1049 "Encrypted file contents should differ from plaintext JSON"
1050 );
1051
1052 assert!(
1054 encrypted_contents.len() > plaintext_json.len(),
1055 "Encrypted file should be larger due to encryption overhead. Encrypted: {}, Plaintext: {}",
1056 encrypted_contents.len(),
1057 plaintext_json.len()
1058 );
1059 }
1060
1061 #[tokio::test]
1062 async fn test_multiple_instances_isolated() {
1063 use crate::storage::EncryptedFilesystemStorage;
1064 use tempfile::TempDir;
1065
1066 let temp_dir = TempDir::new().unwrap();
1067
1068 let token1 = MachineToken::new(
1070 "token_instance_1".to_string(),
1071 "2099-12-31T23:59:59Z".to_string(),
1072 "gw_1".to_string(),
1073 "gateway-1".to_string(),
1074 vec![],
1075 );
1076
1077 let token2 = MachineToken::new(
1078 "token_instance_2".to_string(),
1079 "2099-12-31T23:59:59Z".to_string(),
1080 "gw_2".to_string(),
1081 "gateway-2".to_string(),
1082 vec![],
1083 );
1084
1085 let storage1_path = temp_dir.path().join("instance-1");
1087 let storage2_path = temp_dir.path().join("instance-2");
1088
1089 let storage1 = EncryptedFilesystemStorage::new(&storage1_path)
1090 .await
1091 .unwrap();
1092 let storage2 = EncryptedFilesystemStorage::new(&storage2_path)
1093 .await
1094 .unwrap();
1095
1096 let token1_json = serde_json::to_vec(&token1).unwrap();
1098 let token2_json = serde_json::to_vec(&token2).unwrap();
1099 storage1
1100 .write_file_str("auth.json", &token1_json)
1101 .await
1102 .unwrap();
1103 storage2
1104 .write_file_str("auth.json", &token2_json)
1105 .await
1106 .unwrap();
1107
1108 let path1 = storage1_path.join("auth.json");
1110 let path2 = storage2_path.join("auth.json");
1111
1112 assert!(path1.exists(), "Instance 1 token file should exist");
1113 assert!(path2.exists(), "Instance 2 token file should exist");
1114 assert_ne!(path1, path2, "Token files should be in different locations");
1115
1116 let key1_path = storage1_path.join("encryption.key");
1118 let key2_path = storage2_path.join("encryption.key");
1119
1120 if key1_path.exists() && key2_path.exists() {
1121 let key1_contents = std::fs::read(&key1_path).unwrap();
1122 let key2_contents = std::fs::read(&key2_path).unwrap();
1123 assert_ne!(
1124 key1_contents, key2_contents,
1125 "Encryption keys should be different for each instance"
1126 );
1127 }
1128
1129 let decrypted1 = storage1.read_file_str("auth.json").await.unwrap();
1131 let decrypted2 = storage2.read_file_str("auth.json").await.unwrap();
1132
1133 let loaded1: MachineToken = serde_json::from_slice(&decrypted1).unwrap();
1134 let loaded2: MachineToken = serde_json::from_slice(&decrypted2).unwrap();
1135
1136 assert_eq!(loaded1.machine_token, "token_instance_1");
1137 assert_eq!(loaded1.gateway_code, "gateway-1");
1138 assert_eq!(loaded2.machine_token, "token_instance_2");
1139 assert_eq!(loaded2.gateway_code, "gateway-2");
1140 }
1141
1142 #[tokio::test]
1143 #[cfg(unix)]
1144 async fn test_encryption_key_file_permissions() {
1145 use crate::storage::EncryptedFilesystemStorage;
1146 use std::os::unix::fs::PermissionsExt;
1147 use tempfile::TempDir;
1148
1149 let instance_id = "test-key-permissions";
1150 let temp_dir = TempDir::new().unwrap();
1151 let storage_path = temp_dir.path().join(instance_id);
1152
1153 let _storage = EncryptedFilesystemStorage::new(&storage_path)
1155 .await
1156 .unwrap();
1157
1158 let key_path = storage_path.join("encryption.key");
1160
1161 if !key_path.exists() {
1163 return;
1165 }
1166
1167 let metadata = std::fs::metadata(&key_path).unwrap();
1168 let permissions = metadata.permissions();
1169 let mode = permissions.mode();
1170
1171 let permission_bits = mode & 0o777;
1173 assert_eq!(
1174 permission_bits, 0o600,
1175 "Encryption key file should have 0600 permissions (owner read/write only), got {:o}",
1176 permission_bits
1177 );
1178 }
1179
1180 #[tokio::test]
1181 async fn test_tampered_token_file_fails_to_load() {
1182 use crate::storage::EncryptedFilesystemStorage;
1183 use tempfile::TempDir;
1184
1185 let instance_id = "test-tamper";
1186 let temp_dir = TempDir::new().unwrap();
1187 let storage_path = temp_dir.path().join(instance_id);
1188
1189 let token = MachineToken::new(
1190 "original_token".to_string(),
1191 "2099-12-31T23:59:59Z".to_string(),
1192 "gw_tamper".to_string(),
1193 "tamper-test".to_string(),
1194 vec![],
1195 );
1196
1197 let storage = EncryptedFilesystemStorage::new(&storage_path)
1199 .await
1200 .unwrap();
1201 let token_json = serde_json::to_vec(&token).unwrap();
1202 storage
1203 .write_file_str("auth.json", &token_json)
1204 .await
1205 .unwrap();
1206
1207 let token_path = storage_path.join("auth.json");
1209 let mut contents = std::fs::read(&token_path).unwrap();
1210
1211 if contents.len() > 50 {
1213 contents[25] = contents[25].wrapping_add(1);
1214 contents[30] = contents[30].wrapping_sub(1);
1215 std::fs::write(&token_path, contents).unwrap();
1216 }
1217
1218 let result = storage.read_file_str("auth.json").await;
1220 assert!(
1221 result.is_err(),
1222 "Loading tampered encrypted file should fail"
1223 );
1224 }
1225
1226 #[tokio::test]
1231 #[serial]
1232 async fn test_generic_save_and_load_user_token() {
1233 use crate::runbeam_api::types::UserToken;
1234 let _guard = setup_test_encryption();
1235 let instance_id = "test-user-token";
1236 clear_token(instance_id, "user_auth").await.ok();
1237
1238 let user_token = UserToken::new(
1239 "user_jwt_token".to_string(),
1240 Some(1234567890),
1241 Some(crate::runbeam_api::types::UserInfo {
1242 id: "user123".to_string(),
1243 name: "Test User".to_string(),
1244 email: "test@example.com".to_string(),
1245 }),
1246 );
1247
1248 save_token(instance_id, "user_auth", &user_token)
1250 .await
1251 .unwrap();
1252
1253 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1255 assert!(loaded.is_some());
1256
1257 let loaded_token = loaded.unwrap();
1258 assert_eq!(loaded_token.token, user_token.token);
1259 assert_eq!(loaded_token.expires_at, user_token.expires_at);
1260 assert!(loaded_token.user.is_some());
1261
1262 clear_token(instance_id, "user_auth").await.unwrap();
1264 }
1265
1266 #[tokio::test]
1267 #[serial]
1268 async fn test_different_token_types_isolated() {
1269 use crate::runbeam_api::types::UserToken;
1270 let _guard = setup_test_encryption();
1271 let instance_id = "test-isolation";
1272
1273 let user_token = UserToken::new("user_token".to_string(), None, None);
1275
1276 let machine_token = MachineToken::new(
1277 "machine_token".to_string(),
1278 "2099-12-31T23:59:59Z".to_string(),
1279 "gw_test".to_string(),
1280 "test-gw".to_string(),
1281 vec![],
1282 );
1283
1284 save_token(instance_id, "user_auth", &user_token)
1286 .await
1287 .unwrap();
1288 save_token(instance_id, "auth", &machine_token)
1289 .await
1290 .unwrap();
1291
1292 let loaded_user: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1294 let loaded_machine: Option<MachineToken> = load_token(instance_id, "auth").await.unwrap();
1295
1296 assert!(loaded_user.is_some());
1297 assert!(loaded_machine.is_some());
1298 assert_eq!(loaded_user.unwrap().token, "user_token");
1299 assert_eq!(loaded_machine.unwrap().machine_token, "machine_token");
1300
1301 clear_token(instance_id, "user_auth").await.unwrap();
1303 clear_token(instance_id, "auth").await.unwrap();
1304 }
1305
1306 #[tokio::test]
1307 #[serial]
1308 async fn test_user_token_with_full_metadata() {
1309 use crate::runbeam_api::types::UserToken;
1310 let _guard = setup_test_encryption();
1311 let instance_id = "test-user-full";
1312 clear_token(instance_id, "user_auth").await.ok();
1313
1314 let user_token = UserToken::new(
1315 "detailed_user_token".to_string(),
1316 Some(2000000000),
1317 Some(crate::runbeam_api::types::UserInfo {
1318 id: "user456".to_string(),
1319 name: "John Doe".to_string(),
1320 email: "john@example.com".to_string(),
1321 }),
1322 );
1323
1324 save_token(instance_id, "user_auth", &user_token)
1326 .await
1327 .unwrap();
1328 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1329
1330 assert!(loaded.is_some());
1331 let loaded_token = loaded.unwrap();
1332 assert_eq!(loaded_token.token, "detailed_user_token");
1333 assert_eq!(loaded_token.expires_at, Some(2000000000));
1334
1335 let user_info = loaded_token.user.unwrap();
1336 assert_eq!(user_info.id, "user456");
1337 assert_eq!(user_info.name, "John Doe");
1338 assert_eq!(user_info.email, "john@example.com");
1339
1340 clear_token(instance_id, "user_auth").await.unwrap();
1342 }
1343}