tauri-apps / tauri

Build smaller, faster, and more secure desktop and mobile applications with a web frontend.
https://tauri.app
Apache License 2.0
82.91k stars 2.49k forks source link

[feat] Re-design the Tauri APIs around capability-based security #6107

Closed JonasKruckenberg closed 2 months ago

JonasKruckenberg commented 1 year ago

Describe the problem

With mobile on the horizon we face the problem that most of Tauri's APIs do not work on mobile. This has several reasons:

  1. Lacking support in upstream crates. A lot of upstream dependence of Tauri don't support iOS and Android (e.g. dirs-next)
  2. Mobile OS'es work differently. Mobile OS'es impose a lot more restrictions on apps and have stronger sandboxes. In addition more APIs require explicit user action (like selecting files etc.) and can be fallible.

Problem one can be resolved by contributing fixes, working with upstream authors, forking or replacing crates etc. But problem two is a major blocker in my opinion. While Tauri can technically run on both iOS and Android (which is already frigging awesome!) theres not much you can do with it yet.

Note: The distinction between not supported (yet) and incompatible is very important. I will focus on the latter going forward as that is the real issue: APIs that are conceptually incompatible with how mobile OS'es behave.

Scopes

As my proposal going forward focuses mostly on permissions, I want to bring up another big problem in the current implementation: Scopes.

  1. Scopes are hard to understand, reason about and easy to get wrong. Even a slight typo in the scope pattern will either break the app at runtime or worse: lead to a security vulnerability. There is also absolutely no feedback to the user if they made an error in their scope pattern.
  2. Scope patterns are not flexible enough. They don't account for multi-window setups, and offer no way to restrict read/write permissions. As you will see later on scope patterns are also less powerfull than capabilities.

Describe the solution you'd like

Since we need to re-design large parts of the API anyway, due to the aforementioned mobile compatibility issues we should seize this opportunity of that major bump and redesign our API in a way that makes Tauri applications much more secure by default.

What is the solution I am proposing?

We redesign the API from the ground up with two Concepts in mind: Capability-based security and Host/Guests. A big inspiration here is the WASI specification.

Host/Guest

This term comes from Virtual Machines and just formalizes how we have been thinking about Tauri. The Host is the trusted Rust process that is at the heart of a Tauri app. It owns all resources and has unfettered access to the OS. A Guest is a process that is connected to the Host through the IPC bridge. It may request access to resources, and ask the Host to perform actions on its behalf. We currently consider the WebView to be the Guest but going forward we consider each window to be a Guest. Another example of a Guest would be a running WASM extension, or a running Deno v8 isolate.

Screenshot 2023-01-19 at 13 50 19

Capability-based security

Capabilities is an unforgeable references to something the Host owns. A Guest may request a reference to that resource, which is represented by a random integer. That integer is used a key in the Resource Table (resource_table = HashMap<u64, Resource>). The key takeaways are:

I want us to also consider this as an overarching mental model of the whole crate, all interactions fall neatly into Host/Guest relations that can be modeled using capabilities. This opens the door for implementing much requested features like WASM runtime extensions and Deno support in a uniform and easy to reason about manner.

It also allows us model the more restrictive APIs on iOS and Android in an elegant way. In fact, it increases cross-platform security by implementing the strictest-common-denominator and giving us much more fine-grained control.

I believe the proposed solution is one that not only advances the current state-of-the-art in app security, but also one that fullfills Tauris goal of being the leader is security.

Alternatives considered

No response

Additional context

Much of this is based on the design of WASI: https://github.com/WebAssembly/WASI and the implementation of a capabilities-based rust std crate used in the Wasmtime engine: https://github.com/bytecodealliance/cap-std

Example: File-Picker

const files = await open({ ext: [".mp4"] });

// each file is basically a thin wrapper around a random int 
// the Host knows how to actually acess that file on Disk
// the unique File handles can be used to interact with their corresponding file
for (const file in files) {
    const content = await file.readToString();
}

// now we can use the `FinalizationRegistry` internally or manual .close() calls where appropriate

files.forEach(file => file.close()) // will remove the File id from the Hosts lookup table

// optionally when the webview supports it we may use FinalizationRegistry.register` 
// to close file handles when they get garbage-collected

An improved File-Picker API

The above example is a bit bare, so let me show you how we can improve the security of this API even further:

import { open } from '@tauri-apps/api/dialog'
import { Permission } from '@tauri-apps/api/fs'

// opens the files both for reading and writing (the current default)
const files = await open({ 
    ext: [".mp4"], 
    permissions: Permission.Read & Permission.Write 
});

// but we may only want to allow reading of a file 
// (to prevent malicious code from changing our files)
const files = await open({ 
    ext: [".mp4"], 
    permissions: Permission.Read
});

// now all `files` will be read-only until they are closed
files.forEach(file => file.readToString()) // will work
files.forEach(file => file.writeString("foobar")) // will not work

Permission is a simple bitflag like so:

export const Permission = {
    /**
     * For files, permission to read the file.
     * For directories, permission to do `readdir` and access files within the directory.
     */
    Read: 0b00000001,
    /** 
     * For files, permission to mutate the file.
     * For directories, permission to create, remove, and rename items within the directory.
     */
    Write: 0b00000010,
}

we can also drop permissions (but never increase them important!) by callingsetPermissions

import { Permission } from '@tauri-apps/api/fs'

const [fileA, fileB] = await open({ 
    ext: [".mp4"], 
    permissions: Permission.Read & Permission.Write 
});

fileA.setPermissions(Permission.Read)
fileB.setPermissions(Permission.Write)
// fileA is now read-only while fileB is now write-only

// attempting to increase the number of permissions will fail
fileA.setPermissions(Permission.Write) // going to fail
nothingismagick commented 1 year ago

Since this will be a breaking change - if we do this, can we add some static analysis in the cli / console that offers them support when they hit this wall?

JonasKruckenberg commented 1 year ago

Since this will be a breaking change - if we do this, can we add some static analysis in the cli / console that offers them support when they hit this wall?

good point! migration-wise or just general support with this?

nothingismagick commented 1 year ago

Migrations would be a positive way to show good faith and wanting to help people transition to 2.0.

tweidinger commented 1 year ago

I started to research possible ways to integrate the capabilities approach into tauri and it seems like that cap-std and the aysnc version of it come with some limitations[^limitation] we need to further explore.

The big but is that there is no other public crate (known to me) which tries to implement the capabilities approach into rust (async) std and we should take the chance to slowly migrate our API code to facilitate the methods offered by cap-std, cap-aysnc-std and cap-primitives. Trying to implement the whole capabilities approach in our layer (ipc/tauri api) will most likely be patching holes and there will be bypasses and edge cases we will not spot immediately, as our main expertise is not on the OS specific isolation layer.

The lower layer (cap-std/cap-primitives) should take care of the isolation and we should focus on offering the developers with a simple, yet effective way to use the capabilities and ambient authority in Tauri. The current configuration design (tauri.conf.json) with the allow list could be streamlined and used to define the ambient authority for the APIs, where it is supported. Unsupported APIs could be protected with our current approach of trying to limit in the api/ipc layer and we could consider contributing upstream to transition to the cap version of the needed crates.

As we need to re-design our API for supporting all operating systems properly, as mentioned in the initial issue post, this could be our chance to figure out where we can plug-in the cap based code and where we need workarounds for now.

[^limitation]: No (official) support for iOS and Android, networking capabilities depend on resolved hosts reference, compatibility with tokio and other async framework is not 1.0 ready yet reference and further testing is required

tweidinger commented 2 months ago

Removed this from 2.0 as we have a new allow list system (named capabilities) and the work to go full "real" capability is not feasible for the next foreseeable future. Marked as stale.