1use anyhow::{Context, Result};
2use base64::{engine::general_purpose::STANDARD, Engine};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6
7const XOR_KEY: &[u8] = b"HowlerWolf2024";
11
12fn xor_obfuscate(data: &[u8]) -> Vec<u8> {
13 data.iter()
14 .enumerate()
15 .map(|(i, b)| b ^ XOR_KEY[i % XOR_KEY.len()])
16 .collect()
17}
18
19pub fn obfuscate(plaintext: &str) -> String {
21 let xored = xor_obfuscate(plaintext.as_bytes());
22 STANDARD.encode(&xored)
23}
24
25pub fn deobfuscate(obfuscated: &str) -> Result<String> {
27 let decoded = STANDARD
28 .decode(obfuscated)
29 .context("Failed to base64-decode token")?;
30 let xored = xor_obfuscate(&decoded);
31 String::from_utf8(xored).context("Failed to convert deobfuscated token to UTF-8")
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct SecretsFile {
36 pub iucn: Option<IucnSecrets>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct IucnSecrets {
41 pub token: String,
42}
43
44pub fn load_iucn_token_from_secrets(secrets_path: &Path) -> Result<Option<String>> {
46 if !secrets_path.exists() {
47 return Ok(None);
48 }
49
50 let content = fs::read_to_string(secrets_path)
51 .with_context(|| format!("Failed to read {}", secrets_path.display()))?;
52
53 let secrets: SecretsFile = toml::from_str(&content).context("Failed to parse secrets.toml")?;
54
55 if let Some(iucn) = secrets.iucn {
56 let token = deobfuscate(&iucn.token)?;
57 Ok(Some(token))
58 } else {
59 Ok(None)
60 }
61}
62
63pub fn save_iucn_token_to_secrets(secrets_path: &Path, token: &str) -> Result<()> {
65 let obfuscated = obfuscate(token);
66
67 let secrets = SecretsFile {
68 iucn: Some(IucnSecrets { token: obfuscated }),
69 };
70
71 let content = toml::to_string_pretty(&secrets).context("Failed to serialize secrets")?;
72
73 if let Some(parent) = secrets_path.parent() {
75 fs::create_dir_all(parent)?;
76 }
77
78 fs::write(secrets_path, content)
79 .with_context(|| format!("Failed to write {}", secrets_path.display()))?;
80
81 Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_obfuscate_deobfuscate_roundtrip() {
90 let original = "my_secret_iucn_token_12345";
91 let obfuscated = obfuscate(original);
92 let deobfuscated = deobfuscate(&obfuscated).unwrap();
93 assert_eq!(original, deobfuscated);
94 }
95
96 #[test]
97 fn test_obfuscate_produces_different_output() {
98 let original = "test_token";
99 let obfuscated = obfuscate(original);
100 assert_ne!(original, obfuscated);
101 }
102
103 #[test]
104 fn test_deobfuscate_invalid_base64() {
105 let result = deobfuscate("not-valid-base64!!!");
106 assert!(result.is_err());
107 }
108
109 #[test]
110 fn test_save_and_load_secrets() {
111 let dir = tempfile::tempdir().unwrap();
112 let path = dir.path().join("secrets.toml");
113
114 save_iucn_token_to_secrets(&path, "test_iucn_token").unwrap();
115 let loaded = load_iucn_token_from_secrets(&path).unwrap();
116 assert_eq!(loaded, Some("test_iucn_token".to_string()));
117 }
118}