From 37264fdf601459fa60600eb8b4ded2bc312f521d Mon Sep 17 00:00:00 2001 From: Daniel Oppermann Date: Fri, 26 Apr 2024 21:08:34 +0200 Subject: [PATCH] fix: fix #2 and add runtime feature checks --- Cargo.toml | 9 ++- README.md | 38 +++++++----- build.rs | 3 +- examples/svelte-app/src-tauri/Cargo.toml | 2 +- .../src-tauri/capabilities/default.json | 3 +- examples/svelte-app/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- .../commands/feature_check_command.toml | 13 ++++ permissions/autogenerated/reference.md | 2 + permissions/schemas/schema.json | 14 +++++ src/helper.rs | 21 +++++++ src/lib.rs | 40 +++++++++--- src/licensing.rs | 5 +- src/service.rs | 6 +- webview-src/helper.ts | 34 ++++++++++ webview-src/licensing.ts | 62 ++++++++++++++----- webview-src/service.ts | 23 ++++--- 17 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 permissions/autogenerated/commands/feature_check_command.toml create mode 100644 webview-src/helper.ts diff --git a/Cargo.toml b/Cargo.toml index d713e6f..59c7fbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "protectus" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Dan0xE "] exclude = ["/webview-src", "/examples"] @@ -16,4 +16,9 @@ base64 = "0.22" chrono = "0.4" [build-dependencies] -tauri-plugin = { version = "2.0.0-beta", features = ["build"] } \ No newline at end of file +tauri-plugin = { version = "2.0.0-beta", features = ["build"] } + +[features] +default = ["service"] +service = [] +licensing = [] \ No newline at end of file diff --git a/README.md b/README.md index ad173e7..00262fc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # Protectus -> [!WARNING] -> Currently only works with VmProtect Ultimate, see [#2](https://github.com/Dan0xE/protectus/issues/2) - -Call VmProtect functions directly from your Tauri WebView. - ---- ## Supported functions @@ -23,8 +17,24 @@ This will be expanded in the near future! ## Tauri Plugin Permissions +> [!IMPORTANT] +> Protectus performs feature checks at runtime before executing any function. +> This is done to verify whether a feature that the called function depends on (for example, 'licensing') +> is enabled in the Rust backend, in order to prevent unwanted behavior. +> Without adding the following permission, Protectus will not function properly! + +```json +"permissions": [ + "protectus:allow-feature-check-command" +] +``` + +Protectus Permission List: + + ```json "permissions": [ + "protectus:allow-feature-check-command" "protectus:allow-is-debugger-present-command", "protectus:allow-is-protected-command", "protectus:allow-is-virtual-machine-command", @@ -42,17 +52,15 @@ This will be expanded in the near future! - VmProtect Installation - Tauri Version >= 2.0.0-beta -When using a Debug Build of your Application, please copy the following files from VmProtect into your target/debug folder: +When using a Debug Build of your application, copy either all files or only the files necessary for your operating system and architecture from VmProtect into your `target/debug` folder: -- libVMProtectSDK.dylib -- libVMProtectSDK32.so -- libVMProtectSDK64.so -- VMProtectSDK32.dll -- VMProtectSDK32.lib -- VMProtectSDK64.dll -- VMProtectSDK64.lib +- For macOS: `libVMProtectSDK.dylib` +- For 32-bit Linux: `libVMProtectSDK32.so` +- For 64-bit Linux: `libVMProtectSDK64.so` +- For 32-bit Windows: `VMProtectSDK32.dll`, `VMProtectSDK32.lib` +- For 64-bit Windows: `VMProtectSDK64.dll`, `VMProtectSDK64.lib` -## Limitations of the Demo +## Limitations when using Debug Builds Some functions do not return a valid result until the Application is protected with VmProtect diff --git a/build.rs b/build.rs index d8f4ca8..b90fe5c 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,7 @@ pub fn main() { "deactivate_license_command", "is_protected_command", "is_virtual_machine_command", - "is_debugger_present_command" + "is_debugger_present_command", + "feature_check_command" ]).build(); } diff --git a/examples/svelte-app/src-tauri/Cargo.toml b/examples/svelte-app/src-tauri/Cargo.toml index 8f2783d..8525d52 100644 --- a/examples/svelte-app/src-tauri/Cargo.toml +++ b/examples/svelte-app/src-tauri/Cargo.toml @@ -15,6 +15,6 @@ tauri = { version = "2.0.0-beta", features = [] } tauri-plugin-shell = "2.0.0-beta" serde = { version = "1", features = ["derive"] } serde_json = "1" -protectus = { path = "../../../"} +protectus = { path = "../../../", features = ["licensing"] } tauri-plugin-dialog = "2.0.0-beta.4" diff --git a/examples/svelte-app/src-tauri/capabilities/default.json b/examples/svelte-app/src-tauri/capabilities/default.json index e37e496..9eee723 100644 --- a/examples/svelte-app/src-tauri/capabilities/default.json +++ b/examples/svelte-app/src-tauri/capabilities/default.json @@ -25,6 +25,7 @@ "protectus:allow-get-serial-number-state-command", "protectus:allow-activate-license-command", "protectus:allow-deactivate-license-command", - "protectus:allow-set-serial-number-command" + "protectus:allow-set-serial-number-command", + "protectus:allow-feature-check-command" ] } \ No newline at end of file diff --git a/examples/svelte-app/src-tauri/tauri.conf.json b/examples/svelte-app/src-tauri/tauri.conf.json index abeab78..cb421ed 100644 --- a/examples/svelte-app/src-tauri/tauri.conf.json +++ b/examples/svelte-app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "svelte-app", "version": "0.0.0", - "identifier": "com.tauri.dev", + "identifier": "com.protectus.dev", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", diff --git a/package.json b/package.json index 14a8377..15574d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "protectus", - "version": "0.1.1", + "version": "0.1.2", "main": "webview-dist/index.js", "license": "MIT", "dependencies": { diff --git a/permissions/autogenerated/commands/feature_check_command.toml b/permissions/autogenerated/commands/feature_check_command.toml new file mode 100644 index 0000000..8ce2778 --- /dev/null +++ b/permissions/autogenerated/commands/feature_check_command.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-feature-check-command" +description = "Enables the feature_check_command command without any pre-configured scope." +commands.allow = ["feature_check_command"] + +[[permission]] +identifier = "deny-feature-check-command" +description = "Denies the feature_check_command command without any pre-configured scope." +commands.deny = ["feature_check_command"] diff --git a/permissions/autogenerated/reference.md b/permissions/autogenerated/reference.md index f54dab4..dbebf08 100644 --- a/permissions/autogenerated/reference.md +++ b/permissions/autogenerated/reference.md @@ -4,6 +4,8 @@ |`deny-activate-license-command`|Denies the activate_license_command command without any pre-configured scope.| |`allow-deactivate-license-command`|Enables the deactivate_license_command command without any pre-configured scope.| |`deny-deactivate-license-command`|Denies the deactivate_license_command command without any pre-configured scope.| +|`allow-feature-check-command`|Enables the feature_check_command command without any pre-configured scope.| +|`deny-feature-check-command`|Denies the feature_check_command command without any pre-configured scope.| |`allow-get-hwid-command`|Enables the get_hwid_command command without any pre-configured scope.| |`deny-get-hwid-command`|Denies the get_hwid_command command without any pre-configured scope.| |`allow-get-serial-number-data-command`|Enables the get_serial_number_data_command command without any pre-configured scope.| diff --git a/permissions/schemas/schema.json b/permissions/schemas/schema.json index a0214aa..9ae1c46 100644 --- a/permissions/schemas/schema.json +++ b/permissions/schemas/schema.json @@ -322,6 +322,20 @@ "deny-deactivate-license-command" ] }, + { + "description": "allow-feature-check-command -> Enables the feature_check_command command without any pre-configured scope.", + "type": "string", + "enum": [ + "allow-feature-check-command" + ] + }, + { + "description": "deny-feature-check-command -> Denies the feature_check_command command without any pre-configured scope.", + "type": "string", + "enum": [ + "deny-feature-check-command" + ] + }, { "description": "allow-get-hwid-command -> Enables the get_hwid_command command without any pre-configured scope.", "type": "string", diff --git a/src/helper.rs b/src/helper.rs index c42540e..ba362e7 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,6 +1,7 @@ use std::time::Duration; use serde::Serialize; +use tauri::command; use vmprotect::licensing::{ActivationStatus, SerialNumberData, SerialState}; #[derive(Serialize)] @@ -68,3 +69,23 @@ pub fn activation_status_to_error_message(status: ActivationStatus) -> String { ActivationStatus::NulError => "The license contains a NUL character.".to_string(), } } + +#[derive(Serialize)] +pub struct Features { + pub licensing: bool, + pub service: bool, +} + +impl Features { + pub fn new() -> Features { + Features { + licensing: cfg!(feature = "licensing"), + service: cfg!(feature = "service"), + } + } +} + +#[command] +pub fn feature_check_command() -> Result { + Ok(Features::new()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bea8acb..7415e97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,51 @@ pub mod helper; +#[cfg(feature = "licensing")] pub mod licensing; +#[cfg(feature = "service")] pub mod service; +use serde::Serialize; use tauri::plugin::{Builder, TauriPlugin}; -use tauri::Runtime; +use tauri::{command, Runtime}; + +#[cfg(feature = "licensing")] use crate::licensing::*; +#[cfg(feature = "service")] use crate::service::*; +use helper::feature_check_command; + + + + pub fn init() -> TauriPlugin { - Builder::new("protectus") - .invoke_handler(tauri::generate_handler![ + let mut builder = Builder::new("protectus"); + + #[cfg(feature = "licensing")] + { + use crate::licensing::*; + builder = builder.invoke_handler(tauri::generate_handler![ + feature_check_command, get_hwid_command, set_serial_number_command, get_serial_number_state_command, get_serial_number_data_command, activate_license_command, deactivate_license_command, - is_protected_command, + ]); + } + + #[cfg(feature = "service")] + { + use crate::service::*; + builder = builder.invoke_handler(tauri::generate_handler![ + feature_check_command, is_virtual_machine_command, - is_debugger_present_command - ]) - .build() + is_debugger_present_command, + is_protected_command + ]); + } + + builder.build() } diff --git a/src/licensing.rs b/src/licensing.rs index 2b96ae0..1686245 100644 --- a/src/licensing.rs +++ b/src/licensing.rs @@ -11,10 +11,11 @@ pub fn get_hwid_command() -> Result { Ok(get_hwid()) } + #[command] pub fn set_serial_number_command(serial_str: String) -> Result { let serial_bytes = serial_str.into_bytes(); - + match set_serial_number(serial_bytes) { Ok(serial_state) => { let states = serialize_serial_state(serial_state); @@ -27,7 +28,7 @@ pub fn set_serial_number_command(serial_str: String) -> Result { } #[command] -pub fn get_serial_number_state_command() -> Result { +pub fn get_serial_number_state_command() -> Result { let serial_state = get_serial_number_state(); let states = serialize_serial_state(serial_state); Ok(format!("Serial state: {}", states.join(", "))) diff --git a/src/service.rs b/src/service.rs index d65b365..63aa137 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,16 +2,16 @@ use tauri::command; use vmprotect::service::{is_debugger_present, is_protected, is_virtual_machine}; #[command] -pub fn is_protected_command() -> Result { +pub fn is_protected_command() -> Result { Ok(is_protected()) } #[command] -pub fn is_virtual_machine_command() -> Result { +pub fn is_virtual_machine_command() -> Result { Ok(is_virtual_machine()) } #[command] -pub fn is_debugger_present_command(check_kernel_mode: bool) -> Result { +pub fn is_debugger_present_command(check_kernel_mode: bool) -> Result { Ok(is_debugger_present(check_kernel_mode)) } diff --git a/webview-src/helper.ts b/webview-src/helper.ts new file mode 100644 index 0000000..7612ae9 --- /dev/null +++ b/webview-src/helper.ts @@ -0,0 +1,34 @@ +import {invoke} from '@tauri-apps/api/core'; + +interface Features { + licensing: boolean; + service: boolean; +} + +type GenericFunc = () => Promise; + +/** Checks if the required feature for the passed function is enabled on the rust backend + * @param {keyof Features} featureEnabled Feature that has to be enabled for this function to be executed + * @param {GenericFunc[]} functionsToExecute Functions that should be executed if the required feature is enabled + * @throws Throws if the required feature is not enabled in the rust backend + */ +export async function featureGate( + featureEnabled: keyof Features, + ...functionsToExecute: GenericFunc[] +): Promise { + const features: Features = await invoke( + 'plugin:protectus|feature_check_command' + ); + + if (features[featureEnabled]) { + let lastResult: F | undefined = undefined; + for (const fn of functionsToExecute) { + lastResult = await fn(); + } + return lastResult; + } + + throw new Error( + `${functionsToExecute} called but required feature ${featureEnabled} is not enabled!` + ); +} diff --git a/webview-src/licensing.ts b/webview-src/licensing.ts index b513fba..f2ab355 100644 --- a/webview-src/licensing.ts +++ b/webview-src/licensing.ts @@ -1,39 +1,55 @@ import {invoke} from '@tauri-apps/api/core'; import {SerializableSerialNumberData} from './interface'; +import {featureGate} from './helper'; //a more in-depth and more accurate documentation can be found here: https://vmpsoft.com/vmprotect/user-manual/ /** Gets the Users Hardware ID * @returns {Promise} The Users Hardware ID */ -export async function getHardwareId(): Promise { - return await invoke('plugin:protectus|get_hwid_command'); +export async function getHardwareId(): Promise { + return await featureGate('licensing', async () => + invoke('plugin:protectus|get_hwid_command') + ); } /** Sets the given Serial Number * @param {string} serialStr Serial Number to set * @returns {Promise} Operation Result */ -export async function setSerialNumber(serialStr: string): Promise { - return await invoke('plugin:protectus|set_serial_number_command', { - serialStr, - }); +export async function setSerialNumber( + serialStr: string +): Promise { + return await featureGate('licensing', async () => + invoke('plugin:protectus|set_serial_number_command', { + serialStr, + }) + ); } /** Gets the Serial Number State * @returns {Promise} Serial Number State */ -export async function getSerialNumberState(): Promise { - return invoke('plugin:protectus|get_serial_number_state_command'); +export async function getSerialNumberState(): Promise { + return featureGate('licensing', async () => + invoke('plugin:protectus|get_serial_number_state_command') + ); } /** Gets the Serial Number Data * @returns {Promise} Serialized Serial Number Data */ -export async function getSerialNumberData(): Promise { - const data: SerializableSerialNumberData = await invoke( - 'plugin:protectus|get_serial_number_data_command' +export async function getSerialNumberData(): Promise< + SerializableSerialNumberData | undefined +> { + const data: SerializableSerialNumberData | undefined = await featureGate( + 'licensing', + async () => invoke('plugin:protectus|get_serial_number_data_command') ); + + //we only check for undefined here + if (data === undefined) throw new Error('No Serial Number Data returned!'); + if (data.user_data) { const decodedUserData = atob(data.user_data); data.user_data = decodedUserData; @@ -45,16 +61,28 @@ export async function getSerialNumberData(): Promise} Activation Result */ -export async function activateLicense(code: string): Promise { - return await invoke('plugin:protectus|activate_license_command', {code}); +export async function activateLicense( + code: string +): Promise { + return featureGate( + 'licensing', + async () => + await invoke('plugin:protectus|activate_license_command', {code}) + ); } /** Deactivates a given License Code * @param {string} serial License Code to Deactivate * @returns {Promise} Operation Result */ -export async function deactivateLicense(serial: string): Promise { - return await invoke('plugin:protectus|deactivate_license_command', { - serial, - }); +export async function deactivateLicense( + serial: string +): Promise { + return featureGate( + 'licensing', + async () => + await invoke('plugin:protectus|deactivate_license_command', { + serial, + }) + ); } diff --git a/webview-src/service.ts b/webview-src/service.ts index d1c1d26..daea8b8 100644 --- a/webview-src/service.ts +++ b/webview-src/service.ts @@ -1,17 +1,22 @@ import {invoke} from '@tauri-apps/api/core'; +import {featureGate} from './helper'; /** Checks if the Application is protected with VmProtect * @returns {Promise} Protection State */ -export async function checkIfProtected(): Promise { - return await invoke('plugin:protectus|is_protected_command'); +export async function checkIfProtected(): Promise { + return await featureGate('service', async () => + invoke('plugin:protectus|is_protected_command') + ); } /** Checks if the Application is running in a Virtual Machine * @returns {Promise} Running in a Virtual Machine */ -export async function checkIfVirtualMachine(): Promise { - return await invoke('plugin:protectus|is_virtual_machine_command'); +export async function checkIfVirtualMachine(): Promise { + return await featureGate('service', async () => + invoke('plugin:protectus|is_virtual_machine_command') + ); } /** Checks if a debugger is attached to the Application @@ -20,8 +25,10 @@ export async function checkIfVirtualMachine(): Promise { */ export async function checkIfDebuggerPresent( checkKernelMode: boolean -): Promise { - return invoke('plugin:protectus|is_debugger_present_command', { - checkKernelMode, - }); +): Promise { + return featureGate('service', async () => + invoke('plugin:protectus|is_debugger_present_command', { + checkKernelMode, + }) + ); }