Tauri V2 Overview
Tauri V2 is still in beta at the time of this note is written, so there may be changes to missing details, while the core concept and features should stay the same.
IPC
The process model is the same as Tauri V1.
The core written in Rust is a process, and each webview window is a separate process. Events and commands are used to communicate between the core and the webview windows.
Brownfield Pattern
Wikipedia: Brownfield (software development)
Simplest pattern to use Tauri as it tries to be compatible with existing frontend projects out of the box. No additional configuration is needed.
{
"tauri": {
"pattern": {
"use": "brownfield"
}
}
}
Isolation Pattern
The Isolation pattern is a way to intercept and modify Tauri API messages sent by the frontend before they get to Tauri Core, all with JavaScript. The secure JavaScript code that is injected by the Isolation pattern is referred to as the Isolation application.
There may be untrusted code running in the frontend. For example, if a dependency package has malicous code, it could potentially access the Tauri API and do harm to the system. Or if a plugin system is implemented to let user load their favourite plugins, the plugins could potentially access OS APIs through Tauri API and do harm to the system.
With the isolation pattern, you can intercept requests to Tauri core. e.g. Verify IPC inputs.
All messages from frontend are intercepted, including events and commands.
How
The "secure application" is injected between frontend and Tauri Core to intercept and modify IPC messages.
<iframe>
's sandboxing feature is used to run JS securely alongside the main frontend app. All IPC calls to core are routed through the sandboxed isolation app. Messages are encrypted with browser's SubtleCrypto.
New encryption keys are generated every time the app starts.
Usage
Construct an html that will be loaded to iframe with its JS code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isolation Secure Script</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
// let's not verify or modify anything, just print the content from the hook
console.log("hook", payload);
return payload;
};
{
"build": {
"distDir": "../dist"
},
"tauri": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../dist-isolation"
}
}
}
}
Security
Trust Boundaries
IPC is the bridge between 2 trusted groups and need to ensure that boundaries are not broken.
Since plugin and core API have full access to system, any untrusted code running in the frontend could potentially access the system. Access to core commands is restricted by capabilities defined in app config. Individual command constraint can be set in config for more fine-grained access levels.
Capabilities
Capabilities are a set of permissions mapped to app windows and webviews by their labels.
Capability files are either defined as a JSON or a TOML file inside the src-tauri/capabilities
directory.
Sample config enable default functionality for core plugin and window.setTitle
API.
{
"$schema": "./schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"window:allow-set-title"
]
}
This file needs to be added to the tauri.conf.json
file.
{
"app": {
"security": {
"capabilities": ["my-capability", "main-capability"]
}
}
}
Capabilities can be defined as an inline object, instead of a file.
{
"app": {
"security": {
"capabilities": [
{
"identifier": "my-capability",
"description": "My application capability used for all windows",
"windows": ["*"],
"permissions": ["fs:default", "allow-home-read-extended"]
},
"my-second-capability"
]
}
}
}
Target Platform
Capability is by default applied to all platforms, but can be specified separately.
- Desktop
- Mobile
{
"$schema": "./schemas/desktop-schema.json",
"identifier": "desktop-capability",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": ["global-shortcut:allow-register"]
}
{
"$schema": "./schemas/mobile-schema.json",
"identifier": "mobile-capability",
"windows": ["main"],
"platforms": ["iOS", "android"],
"permissions": [
"nfc:allow-scan",
"biometric:allow-authenticate",
"barcode-scanner:allow-scan"
]
}
Remote API Access
By default the APIs are only accessible to bundled code shipped with the Tauri App.
To allow remote resources to access the API,
{
"$schema": "./schemas/remote-schema.json",
"identifier": "remote-tag-capability",
"windows": ["main"],
"remote": {
"urls": ["https://*.tauri.app"]
}
"platforms": ["iOS", "android"],
"permissions": [
"nfc:allow-scan",
"barcode-scanner:allow-scan"
]
}
This is similar to dangerousRemoteDomainIpcAccess setting in Tauri V1.
Window can be constrained to access specific url, permissions and platforms can be set.
tauri-app
├── index.html
├── package.json
├── src
├── src-tauri
│ ├── Cargo.toml
│ ├── capabilities
│ └── <identifier>.json/toml
│ ├── src
│ ├── tauri.conf.json
Permissions
Permissions are descriptions of explicit privileges of commands.
[[permission]]
identifier = "my-identifier"
description = "This describes the impact and more."
commands.allow = [
"read_file"
]
[[scope.allow]]
my-scope = "$HOME/*"
[[scope.deny]]
my-scope = "$HOME/secret"
As a plugin developer you can ship multiple, pre-defined, well named permissions for all of your exposed commands.
As an application developer you can extend existing plugin permissions or define them for your own commands. They can be grouped or extended in a set to be re-used or to simplify the main configuration files later.
Permission Identifier
The permissions identifier is used to ensure that permissions can be re-used and have unique names.
<name>:default
Indicates the permission is the default for a plugin or application<name>:<command-name>
Indicates the permission is for an individual command
tauri-plugin
├── README.md
├── src
│ └── lib.rs
├── build.rs
├── Cargo.toml
├── permissions
│ └── <identifier>.json/toml
│ └── default.json/toml
tauri-app
├── index.html
├── package.json
├── src
├── src-tauri
│ ├── Cargo.toml
│ ├── permissions
│ └── <identifier>.toml
| ├── capabilities
│ └── <identifier>.json/.toml
│ ├── src
│ ├── tauri.conf.json
CSP
CSP is used to prevent cross-site-scripting (XSS). i.e. prevent loading of external malicious scripts. If the scripts loaded contains malicious calls to Tauri API, it could potentially harm the system.
So avoid loading remote content such as scripts served over a CDN.
Sample CSP config.
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost",
"font-src": ["https://fonts.gstatic.com"],
"img-src": "'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
Application Lifecycle Threats
- Upstream Threats
- Threats from upstream dependencies, inlcuding dependencies of dependencies.
- Development Threats
- Attacks target development machines, OS, build toolchain and dependencies
- Supply-chain Attacks
- Use git hash or named tags to reference dependencies
- Require contributors to sign commits
- Signing git commits is a way to ensure that the commits you make are actually from you and haven't been tampered with.
- Buildtime Threats
- Use trusted CI/CD
- Sign binaries
- Distribution Threats
- Manifest server / update server, build server, binary hosting service can be compromised.
- Runtime Threats
- Assume WebView is insecure. Use CSP to prevent XSS.
Runtime Authority
The runtime authority is part of the Tauri Core. It holds all permissions, capabilities and scopes at runtime to enforce which window can access which command and passes scopes to commands.
Command Scopes
A scope is a granular way to define (dis)allowed behavior of a Tauri command.
Esxamples
FS Plugin
[[permission]]
identifier = "scope-applocaldata-recursive"
description = '''
This scope recursive access to the complete `$APPLOCALDATA` folder,
including sub directories and files.
'''
[[permission.scope.allow]]
path = "$APPLOCALDATA/**"
[[permission]]
identifier = "deny-webview-data-linux"
description = '''
This denies read access to the
`$APPLOCALDATA` folder on linux as the webview data and
configuration values are stored here.
Allowing access can lead to sensitive information disclosure and
should be well considered.
'''
platforms = ["linux"]
[[scope.deny]]
path = "$APPLOCALDATA/**"
[[permission]]
identifier = "deny-webview-data-windows"
description = '''
This denies read access to the
`$APPLOCALDATA/EBWebView` folder on windows as the webview data and
configuration values are stored here.
Allowing access can lead to sensitive information disclosure and
should be well considered.
'''
platforms = ["windows"]
[[scope.deny]]
path = "$APPLOCALDATA/EBWebView/**"
The above scopes can be used to allow access to the APPLOCALDATA folder, while preventing access to the EBWebView subfolder on windows, which contains sensitive webview data.
Develop
Embedding External Binaries
{
"tauri": {
"bundle": {
"externalBin": [
"/absolute/path/to/sidecar",
"relative/path/to/binary",
"binaries/my-sidecar"
]
}
}
}
Relative path is relative to tauri.config.json
Binaries can be different for each platform, use a -$TARGET_TRIPLE
suffix.
For instance, "externalBin": ["binaries/my-sidecar"]
requires a src-tauri/binaries/my-sidecar-x86_64-unknown-linux-gnu
executable on Linux or src-tauri/binaries/my-sidecar-aarch64-apple-darwin
on Mac OS with Apple Silicon.
To get the target triple, run rustc -Vv | grep host | cut -f2 -d' '
, or rustc -Vv | Select-String "host:" | ForEach-Object {$_.Line.split(" ")[1]}
on Windows.
Binaries can be called with shell plugin in both rust and JS.
Arguments can be passed to the binary, and can be restricted by capabilities.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": [
"arg1",
"-a",
"--arg2",
{
"validator": "\\S+"
},
],
"cmd": "",
"name": "binaries/my-sidecar",
"sidecar": true
}
]
},
"shell:allow-open"
]
}
Regex can be used to validate dynamic args, such as file path or url.
Plugin
use tauri::plugin::{Builder, Runtime, TauriPlugin};
use serde::Deserialize;
// Define the plugin config
#[derive(Deserialize)]
struct Config {
timeout: usize,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
// Make the plugin config optional
// by using `Builder::<R, Option<Config>>` instead
Builder::<R, Config>::new("<plugin-name>")
.setup(|app, api| {
let timeout = api.config.timeout;
Ok(())
})
.build()
}
Lifecycle Events
setup
: Plugin is being initialized- Can be used to manage state, run background tasks
on_navigation
: Web view is attempting to perform navigation- Validate the navigation or track URL changes
on_webview_ready
: New window is being created- Execute init script for every window
on_event
: Event loop events- Handle events such as app exit
on_drop
: Plugin is being deconstructed
Adding Commands
This sample command use dependency injection to access the app handle and window handle, with 2 arguments, on_progress
and url
. on_progress
is a channel to send progress updates to the frontend.
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]
async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) {
// implement command logic here
on_progress.send(100).unwrap();
}
import { invoke, Channel } from '@tauri-apps/api/tauri'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> {
const onProgress = new Channel<number>()
onProgress.onmessage = onProgressHandler
await invoke('plugin:<plugin-name>|upload', { url, onProgress })
}
Command Permissions
Permissions to access commands can be set.
"$schema" = "schemas/schema.json"
[[permission]]
identifier = "allow-start-server"
description = "Enables the start_server command."
commands.allow = ["start_server"]
[[permission]]
identifier = "deny-start-server"
description = "Denies the start_server command."
commands.deny = ["start_server"]
Scopes can be set, and can be accessed in code.
use tauri::ipc::CommandScope;
#[derive(Debug, schemars::JsonSchema)]
pub struct Entry {
pub binary: String,
}
Command Scope
Consumer can define scopes for a command in their capability file. In the plugin, you can read the command-specific scope with the tauri::ipc::CommandScope
struct:
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, command_scope: CommandScope<'_, Entry>) -> Result<()> {
let allowed = command_scope.allows();
let denied = command_scope.denies();
todo!()
}
Global Scope
When a permission does not define any commands to be allowed or denied, it’s considered a scope permission and it should only define a global scope for your plugin:
[[permission]]
identifier = "allow-spawn-node"
description = "This scope permits spawning the `node` binary."
[[permission.scope.allow]]
binary = "node"
You can read the global scope with the tauri::ipc::GlobalScope
struct:
use tauri::ipc::GlobalScope;
use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, scope: GlobalScope<'_, Entry>) -> Result<()> {
let allowed = scope.allows();
let denied = scope.denies();
todo!()
}
#[path = "src/scope.rs"]
mod scope;
const COMMANDS: &[&str] = &[];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.global_scope_schema(schemars::schema_for!(scope::Entry))
.build();
}
In build script, scope entry can be added.
Autogenerated Permissions
Permissions can be autogenerated.
Inside the COMMANDS
const, define the list of commands in snake_case (should match the command function name) and Tauri will automatically generate an allow-$commandname
and a deny-$commandname
permissions.
The following example generates the allow-upload
and deny-upload
permissions:
const COMMANDS: &[&str] = &["upload"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}
Mobile Plugin Development
Plugins can run native mobile code written in Kotlin (or Java) and Swift.
TODO