Skip to main content

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.

Process Model

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.conf.json
{
"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.

../dist-isolation/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isolation Secure Script</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
../dist-isolation/index.js
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;
};
tauri.conf.json
{
"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.

image

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.

src-tauri/capabilities/main.json
{
"$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.

tauri.conf.json
{
"app": {
"security": {
"capabilities": ["my-capability", "main-capability"]
}
}
}

Capabilities can be defined as an inline object, instead of a file.

tauri.conf.json
{
"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.

src-tauri/capabilities/desktop.json
{
"$schema": "./schemas/desktop-schema.json",
"identifier": "desktop-capability",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": ["global-shortcut:allow-register"]
}

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,

src-tauri/capabilities/remote-tags.json
{
"$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.

tauri.conf.json
"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

image

  • 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

image

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

plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml
[[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/**"
plugins/fs/permissions/deny-webview-data.toml
[[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

src-tauri/tauri.conf.json
{
"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.

src-tauri/capabilities/main.json
{
"$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

src/lib.rs
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.

src/commands.rs
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();
}
webview-src/index.ts
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.

permissions/start-server.toml
"$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:

src/commands.rs
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:

permissions/spawn-node.toml
[[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:

src/commands.rs
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!()
}
build.rs
#[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:

build.rs
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