Skip to content

Commit 2647421

Browse files
committed
feat: support encrypted config
1 parent 9bc2b29 commit 2647421

File tree

3 files changed

+81
-16
lines changed

3 files changed

+81
-16
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ Cargo.lock
1818
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
1919
# and can be added to the global gitignore or merged into this file. For a more nuclear
2020
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
21-
#.idea/
21+
#.idea/
22+
23+
config.json

Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wg-multizone"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
edition = "2024"
55

66
[[bin]]
@@ -17,27 +17,27 @@ opt-level = 3
1717
incremental = true
1818

1919
[dependencies]
20-
anyhow = "1.0.97"
20+
anyhow = "1.0.100"
2121
base64 = "0.22.1"
2222
hex = "^0.4.3"
23-
openssl = { version = "0.10.71", features = ["vendored"] }
24-
reqwest = { version = "0.12.15", features = ["json"] }
25-
serde = { version = "1.0.219", features = ["derive"] }
26-
serde_json = "1.0.140"
27-
thiserror = "2.0.12"
28-
tokio = { version = "1.44.1", features = ["full"] }
23+
openssl = { version = "0.10.74", features = ["vendored"] }
24+
reqwest = { version = "0.12.24", features = ["json"] }
25+
serde = { version = "1.0.228", features = ["derive"] }
26+
serde_json = "1.0.145"
27+
thiserror = "2.0.17"
28+
tokio = { version = "1.48.0", features = ["full"] }
2929
tracing = "0.1.41"
30-
tracing-subscriber = "0.3.19"
30+
tracing-subscriber = "0.3.20"
3131
tracing-appender = "^0.2.3"
3232
x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
3333

3434
[dependencies.defguard_wireguard_rs]
3535
package = "defguard_wireguard_rs"
3636
git = "https://github.com/DefGuard/wireguard-rs.git"
37-
rev = "v0.7.1"
37+
rev = "v0.7.8"
3838

3939
[target.'cfg(not(any(target_os = "macos", target_os="windows", target_arch = "arm")))'.dependencies]
40-
tikv-jemallocator = "0.6.0"
40+
tikv-jemallocator = "0.6.1"
4141

4242
[workspace.metadata.cross.target.x86_64-unknown-linux-gnu]
4343
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main-centos"

src/main.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use base64::Engine;
1111
use defguard_wireguard_rs::{
1212
InterfaceConfiguration, WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask,
1313
};
14+
use openssl::symm::{Cipher, Crypter, Mode};
1415
use serde::{Deserialize, Serialize};
1516
use tokio::time::sleep;
1617
use x25519_dalek::{PublicKey, StaticSecret};
@@ -42,6 +43,7 @@ struct NetworkConfig {
4243
#[derive(Debug, Clone, Serialize, Deserialize)]
4344
struct DaemonConfig {
4445
pub config_url: String,
46+
pub secret: Option<String>,
4547
pub fetch_interval: u64,
4648
pub interface_name: String,
4749
}
@@ -65,16 +67,57 @@ fn load_key() -> (PublicKey, StaticSecret) {
6567
(pubkey, secret_key)
6668
}
6769

70+
fn decrypt_config(encrypted: &str, secret: &str) -> Result<String, Box<dyn Error>> {
71+
// Step 1: Base64 decode
72+
let data = base64::engine::general_purpose::STANDARD
73+
.decode(encrypted)
74+
.unwrap();
75+
76+
// Step 2: Verify prefix
77+
assert!(&data[0..8] == b"Salted__");
78+
let salt = &data[8..16];
79+
let ciphertext = &data[16..];
80+
81+
// Step 3: Derive key and iv using PBKDF2-HMAC-SHA256
82+
let mut key = [0u8; 32]; // AES-256 => 32-byte key
83+
let mut iv = [0u8; 16]; // CBC IV
84+
let mut derived = [0u8; 48]; // key + iv = 48 bytes total
85+
86+
openssl::pkcs5::pbkdf2_hmac(
87+
secret.as_bytes(),
88+
salt,
89+
10_000,
90+
openssl::hash::MessageDigest::sha256(),
91+
&mut derived,
92+
)
93+
.unwrap();
94+
key.copy_from_slice(&derived[..32]);
95+
iv.copy_from_slice(&derived[32..48]);
96+
97+
// Step 4: Decrypt
98+
let cipher = Cipher::aes_256_cbc();
99+
let mut crypter = Crypter::new(cipher, Mode::Decrypt, &key, Some(&iv)).unwrap();
100+
crypter.pad(true);
101+
102+
let mut plaintext = vec![0; ciphertext.len() + cipher.block_size()];
103+
let mut count = crypter.update(ciphertext, &mut plaintext).unwrap();
104+
count += crypter.finalize(&mut plaintext[count..]).unwrap();
105+
plaintext.truncate(count);
106+
107+
Ok(String::from_utf8(plaintext)?)
108+
}
109+
68110
async fn fetch_and_apply_config(
69111
wgapi: &WGApi,
70112
interface_config: &mut InterfaceConfiguration,
71113
pubkey: &str,
72114
config: &DaemonConfig,
73115
) -> Result<(), Box<dyn Error>> {
74-
let r = reqwest::get(&config.config_url)
75-
.await?
76-
.json::<NetworkConfig>()
77-
.await?;
116+
let mut r = reqwest::get(&config.config_url).await?.text().await?;
117+
if config.secret.is_some() {
118+
r = decrypt_config(&r, &config.secret.as_ref().unwrap())?;
119+
}
120+
let r: NetworkConfig = serde_json::from_str(&r)?;
78121
let mut my_config: Option<ServerConfig> = None;
79122
for peer in r.peers.iter() {
80123
if peer.pubkey == pubkey {
@@ -200,3 +243,23 @@ async fn main() {
200243
sleep(Duration::from_secs(config.fetch_interval)).await;
201244
}
202245
}
246+
247+
#[cfg(test)]
248+
mod test {
249+
use super::*;
250+
251+
#[tokio::test]
252+
async fn test_decrypt_config() {
253+
let mut r = r#"
254+
U2FsdGVkX19nIrNUt9Wpcyw2qK2rEqJkHX6Wv7ot3sZGR5wIBtkHPvmBXkre46a4
255+
T+8hHiRtwvrZZithpFHi9Y1Tq+T7DrwT4A1auJ15ZZbRSEA5quEl/ywF/65FaDeA
256+
5uhj5lr+BcO8bvLbT7dQzmpAP7rCzY0l067fQh6pNuaiDhK31XnZ0WIK/E+o5k+1
257+
+JwiloAjeMGdP5jNFTws+XjFTPYPJAfhIVdpGqfmb5+hFZh9rZsRTsb+TaGC0tWS
258+
UtXcZz6A4RmXWLx+YgEGUg=="#
259+
.replace("\n", "")
260+
.replace(" ", "");
261+
r = decrypt_config(&r, "mysecret").unwrap();
262+
let r: NetworkConfig = serde_json::from_str(&r).unwrap();
263+
println!("{:?}", r);
264+
}
265+
}

0 commit comments

Comments
 (0)