Skip to main content

howler_core/
crypto.rs

1use anyhow::{Context, Result};
2use base64::{engine::general_purpose::STANDARD, Engine};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6
7/// Simple XOR obfuscation + base64 encoding.
8/// This is NOT encryption — it prevents casual reading of the token.
9/// Anyone with the source code can reverse it.
10const 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
19/// Obfuscate a plaintext token (XOR + base64)
20pub fn obfuscate(plaintext: &str) -> String {
21    let xored = xor_obfuscate(plaintext.as_bytes());
22    STANDARD.encode(&xored)
23}
24
25/// Deobfuscate an obfuscated token (base64 + XOR)
26pub 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
44/// Load obfuscated token from secrets file
45pub 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
63/// Save obfuscated token to secrets file
64pub 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    // Create parent directory if needed
74    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}