mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
refactor: encrypt the sensitive data
This commit is contained in:
@@ -23,7 +23,6 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
webkit2gtk = "0.18.2"
|
||||
regex = "1"
|
||||
url = "2.3"
|
||||
@@ -32,6 +31,10 @@ veil = "0.1.6"
|
||||
whoami = "1.4.1"
|
||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
openssl = "0.10"
|
||||
keyring = "2"
|
||||
aes-gcm = { version = "0.10", features = ["std"] }
|
||||
hex = "0.4"
|
||||
anyhow = "1.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@@ -133,7 +133,7 @@ fn build_window(app_handle: &AppHandle, ua: &str) -> tauri::Result<Window> {
|
||||
Window::builder(app_handle, AUTH_WINDOW_LABEL, url)
|
||||
.visible(false)
|
||||
.title("GlobalProtect Login")
|
||||
.inner_size(400.0, 647.0)
|
||||
.inner_size(600.0, 500.0)
|
||||
.min_inner_size(370.0, 600.0)
|
||||
.user_agent(ua)
|
||||
.always_on_top(true)
|
||||
|
@@ -1,9 +1,11 @@
|
||||
use crate::{
|
||||
auth::{self, AuthData, AuthRequest, SamlBinding, SamlLoginParams},
|
||||
storage::{AppStorage, KeyHint},
|
||||
utils::get_openssl_conf,
|
||||
utils::get_openssl_conf_path,
|
||||
};
|
||||
use gpcommon::{Client, ServerApiError, VpnStatus};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, State};
|
||||
use tokio::fs;
|
||||
@@ -24,9 +26,10 @@ pub(crate) async fn vpn_status<'a>(
|
||||
pub(crate) async fn vpn_connect<'a>(
|
||||
server: String,
|
||||
cookie: String,
|
||||
user_agent: String,
|
||||
client: State<'a, Arc<Client>>,
|
||||
) -> Result<(), ServerApiError> {
|
||||
client.connect(server, cookie).await
|
||||
client.connect(server, cookie, user_agent).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -40,10 +43,10 @@ pub(crate) async fn vpn_disconnect<'a>(
|
||||
pub(crate) async fn saml_login(
|
||||
binding: SamlBinding,
|
||||
request: String,
|
||||
user_agent: String,
|
||||
clear_cookies: bool,
|
||||
app_handle: AppHandle,
|
||||
) -> tauri::Result<Option<AuthData>> {
|
||||
let user_agent = String::from("PAN GlobalProtect");
|
||||
let params = SamlLoginParams {
|
||||
auth_request: AuthRequest::new(binding, request),
|
||||
user_agent,
|
||||
@@ -71,3 +74,29 @@ pub(crate) async fn update_openssl_config(app_handle: AppHandle) -> tauri::Resul
|
||||
fs::write(openssl_conf_path, openssl_conf).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn store_get<'a>(
|
||||
hint: KeyHint<'_>,
|
||||
app_storage: State<'_, AppStorage<'_>>,
|
||||
) -> Result<Option<Value>, ()> {
|
||||
Ok(app_storage.get(hint))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn store_set(
|
||||
hint: KeyHint,
|
||||
value: Value,
|
||||
app_storage: State<'_, AppStorage>,
|
||||
) -> Result<(), tauri_plugin_store::Error> {
|
||||
app_storage.set(hint, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn store_save(
|
||||
app_storage: State<'_, AppStorage>,
|
||||
) -> Result<(), tauri_plugin_store::Error> {
|
||||
app_storage.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
46
gpgui/src-tauri/src/crypto.rs
Normal file
46
gpgui/src-tauri/src/crypto.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use aes_gcm::{
|
||||
aead::{consts::U12, Aead, OsRng},
|
||||
AeadCore, Aes256Gcm, Key, KeyInit, Nonce,
|
||||
};
|
||||
use keyring::Entry;
|
||||
|
||||
const SERVICE_NAME: &str = "GlobalProtect-openconnect";
|
||||
const ENTRY_KEY: &str = "master-key";
|
||||
|
||||
fn get_master_key() -> Result<Key<Aes256Gcm>, anyhow::Error> {
|
||||
let key_entry = Entry::new(SERVICE_NAME, ENTRY_KEY)?;
|
||||
|
||||
if let Ok(key) = key_entry.get_password() {
|
||||
let key = hex::decode(key)?;
|
||||
return Ok(Key::<Aes256Gcm>::clone_from_slice(&key));
|
||||
}
|
||||
|
||||
let key = Aes256Gcm::generate_key(OsRng);
|
||||
let encoded_key = hex::encode(key);
|
||||
|
||||
key_entry.set_password(&encoded_key)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(data: &str) -> Result<String, anyhow::Error> {
|
||||
let master_key = get_master_key()?;
|
||||
let cipher = Aes256Gcm::new(&master_key);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let cipher_text = cipher.encrypt(&nonce, data.as_bytes())?;
|
||||
|
||||
let mut encrypted = nonce.to_vec();
|
||||
encrypted.extend_from_slice(&cipher_text);
|
||||
Ok(hex::encode(encrypted))
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(encrypted: &str) -> Result<String, anyhow::Error> {
|
||||
let master_key = get_master_key()?;
|
||||
let encrypted = hex::decode(encrypted)?;
|
||||
let nonce = Nonce::<U12>::from_slice(&encrypted[..12]);
|
||||
let cipher_text = &encrypted[12..];
|
||||
let cipher = Aes256Gcm::new(&master_key);
|
||||
let plain_text = cipher.decrypt(nonce, cipher_text)?;
|
||||
|
||||
String::from_utf8(plain_text).map_err(|err| err.into())
|
||||
}
|
@@ -2,93 +2,26 @@
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use crate::utils::get_openssl_conf_path;
|
||||
use env_logger::Env;
|
||||
use gpcommon::{Client, ClientStatus, VpnStatus};
|
||||
use log::{info, warn};
|
||||
use serde::Serialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use tauri::{Manager, Wry};
|
||||
use tauri_plugin_log::LogTarget;
|
||||
use tauri_plugin_store::{with_store, StoreCollection};
|
||||
|
||||
mod auth;
|
||||
mod commands;
|
||||
mod crypto;
|
||||
mod settings;
|
||||
mod setup;
|
||||
mod storage;
|
||||
mod utils;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct VpnStatusPayload {
|
||||
status: VpnStatus,
|
||||
}
|
||||
|
||||
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Arc::new(Client::default());
|
||||
let client_clone = client.clone();
|
||||
let app_handle = app.handle();
|
||||
|
||||
let stores = app.state::<StoreCollection<Wry>>();
|
||||
let path = PathBuf::from(".settings.dat");
|
||||
let _ = with_store(app_handle.clone(), stores, path, |store| {
|
||||
let settings_data = store.get("SETTINGS_DATA");
|
||||
let custom_openssl = settings_data.map_or(false, |data| {
|
||||
data["customOpenSSL"].as_bool().unwrap_or(false)
|
||||
});
|
||||
|
||||
if custom_openssl {
|
||||
info!("Using custom OpenSSL config");
|
||||
let openssl_conf = get_openssl_conf_path(&app_handle).into_os_string();
|
||||
std::env::set_var("OPENSSL_CONF", openssl_conf);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
client_clone.subscribe_status(move |client_status| match client_status {
|
||||
ClientStatus::Vpn(vpn_status) => {
|
||||
let payload = VpnStatusPayload { status: vpn_status };
|
||||
if let Err(err) = app_handle.emit_all("vpn-status-received", payload) {
|
||||
warn!("Error emitting event: {}", err);
|
||||
}
|
||||
}
|
||||
ClientStatus::Service(is_online) => {
|
||||
if let Err(err) = app_handle.emit_all("service-status-changed", is_online) {
|
||||
warn!("Error emitting event: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = client_clone.run().await;
|
||||
});
|
||||
|
||||
app.manage(client);
|
||||
|
||||
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||
if desktop == "KDE" {
|
||||
if let Some(main_window) = app.get_window("main") {
|
||||
let _ = main_window.set_decorations(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.targets([
|
||||
LogTarget::LogDir,
|
||||
LogTarget::Stdout, /*LogTarget::Webview*/
|
||||
])
|
||||
.targets([LogTarget::LogDir, LogTarget::Stdout])
|
||||
.level(log::LevelFilter::Info)
|
||||
.with_colors(Default::default())
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.setup(setup)
|
||||
.setup(setup::setup)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::service_online,
|
||||
commands::vpn_status,
|
||||
@@ -98,6 +31,9 @@ fn main() {
|
||||
commands::os_version,
|
||||
commands::openssl_config,
|
||||
commands::update_openssl_config,
|
||||
commands::store_get,
|
||||
commands::store_set,
|
||||
commands::store_save,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
19
gpgui/src-tauri/src/settings.rs
Normal file
19
gpgui/src-tauri/src/settings.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::storage::{AppStorage, KeyHint};
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
const STORAGE_KEY: &str = "SETTINGS_DATA";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Settings {
|
||||
#[serde(rename = "customOpenSSL")]
|
||||
custom_openssl: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn is_custom_openssl_enabled(app_handle: &AppHandle) -> bool {
|
||||
let app_storage = app_handle.state::<AppStorage>();
|
||||
let hint = KeyHint::new(STORAGE_KEY, false);
|
||||
let settings = app_storage.get::<Settings>(hint);
|
||||
|
||||
settings.map_or(false, |settings| settings.custom_openssl)
|
||||
}
|
81
gpgui/src-tauri/src/setup.rs
Normal file
81
gpgui/src-tauri/src/setup.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::{settings, storage::AppStorage, utils::get_openssl_conf_path};
|
||||
use gpcommon::{Client, ClientStatus, VpnStatus};
|
||||
use log::{info, warn};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct VpnStatusPayload {
|
||||
status: VpnStatus,
|
||||
}
|
||||
|
||||
fn setup_window(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP")?;
|
||||
if desktop == "KDE" {
|
||||
if let Some(main_window) = app.get_window("main") {
|
||||
let _ = main_window.set_decorations(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_app_storage(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_handle = app.app_handle();
|
||||
let app_storage = AppStorage::new(app_handle);
|
||||
|
||||
app.manage(app_storage);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_app_env(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_handle = app.app_handle();
|
||||
let use_custom_openssl = settings::is_custom_openssl_enabled(&app_handle);
|
||||
|
||||
if use_custom_openssl {
|
||||
info!("Using custom OpenSSL config");
|
||||
|
||||
let openssl_conf = get_openssl_conf_path(&app_handle).into_os_string();
|
||||
std::env::set_var("OPENSSL_CONF", openssl_conf);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_vpn_client(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_handle = app.handle();
|
||||
let client = Arc::new(Client::default());
|
||||
let client_clone = client.clone();
|
||||
|
||||
app.manage(client_clone);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
client.subscribe_status(move |client_status| match client_status {
|
||||
ClientStatus::Vpn(vpn_status) => {
|
||||
let payload = VpnStatusPayload { status: vpn_status };
|
||||
if let Err(err) = app_handle.emit_all("vpn-status-received", payload) {
|
||||
warn!("Error emitting event: {}", err);
|
||||
}
|
||||
}
|
||||
ClientStatus::Service(is_online) => {
|
||||
if let Err(err) = app_handle.emit_all("service-status-changed", is_online) {
|
||||
warn!("Error emitting event: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = client.run().await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
setup_window(app)?;
|
||||
setup_app_storage(app)?;
|
||||
setup_app_env(app)?;
|
||||
setup_vpn_client(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
87
gpgui/src-tauri/src/storage.rs
Normal file
87
gpgui/src-tauri/src/storage.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::crypto::{decrypt, encrypt};
|
||||
use log::warn;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::fmt::Debug;
|
||||
use tauri::{AppHandle, Manager, Wry};
|
||||
use tauri_plugin_store::{with_store, Error, StoreCollection};
|
||||
|
||||
const STORE_PATH: &str = ".data.json";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct KeyHint<'a> {
|
||||
key: &'a str,
|
||||
encrypted: bool,
|
||||
}
|
||||
|
||||
impl<'a> KeyHint<'a> {
|
||||
pub(crate) fn new(key: &'a str, encrypted: bool) -> Self {
|
||||
Self { key, encrypted }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AppStorage<'a> {
|
||||
path: &'a str,
|
||||
app_handle: AppHandle<Wry>,
|
||||
}
|
||||
|
||||
impl AppStorage<'_> {
|
||||
pub(crate) fn new(app_handle: AppHandle<Wry>) -> Self {
|
||||
Self {
|
||||
path: STORE_PATH,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<T: DeserializeOwned + Debug>(&self, hint: KeyHint) -> Option<T> {
|
||||
let stores = self.app_handle.state::<StoreCollection<Wry>>();
|
||||
with_store(self.app_handle.clone(), stores, self.path, |store| {
|
||||
store
|
||||
.get(hint.key)
|
||||
.ok_or_else(|| Error::Deserialize("Value not found".into()))
|
||||
.and_then(|value| {
|
||||
if !hint.encrypted {
|
||||
return Ok(serde_json::from_value::<T>(value.clone())?);
|
||||
}
|
||||
|
||||
let value = value
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::Deserialize("Value is not a string".into()))?;
|
||||
let value = decrypt(value).map_err(|err| {
|
||||
Error::Deserialize(format!("Failed to decrypt value: {}", err).into())
|
||||
})?;
|
||||
|
||||
Ok(serde_json::from_str::<T>(&value)?)
|
||||
})
|
||||
})
|
||||
.map_err(|err| warn!("Error getting value: {:?}", err))
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn set<T: serde::Serialize>(&self, hint: KeyHint, value: &T) -> Result<(), Error> {
|
||||
let stores = self.app_handle.state::<StoreCollection<Wry>>();
|
||||
|
||||
with_store(self.app_handle.clone(), stores, self.path, |store| {
|
||||
let value = if hint.encrypted {
|
||||
let json_str = serde_json::to_string(value)?;
|
||||
let encrypted = encrypt(&json_str).map_err(|err| {
|
||||
Error::Serialize(format!("Failed to encrypt value: {}", err).into())
|
||||
})?;
|
||||
serde_json::to_value(encrypted)?
|
||||
} else {
|
||||
serde_json::to_value(value)?
|
||||
};
|
||||
|
||||
store.insert(hint.key.to_string(), value)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), Error> {
|
||||
let stores = self.app_handle.state::<StoreCollection<Wry>>();
|
||||
|
||||
with_store(self.app_handle.clone(), stores, self.path, |store| {
|
||||
store.save()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user