Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
JonasKruckenberg opened this issue Jan 19, 2023 · 5 comments
Closed
Labels
type: breaking change This issue or pull request will introduce a breaking change and requires major version bump type: feature request

Comments

@JonasKruckenberg
Copy link
Member

JonasKruckenberg commented Jan 19, 2023

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:

  • Revokable: The Host may revoke a reference at any time by removing the KV pair from its Resource Table. This is currently not possible.
  • Reflect OS State: The Host may remove resources in reponse to user/OS action. E.g. on iOS a user might revoke access to the network at any time through the settings, the Host can update the Resource table to reflect that.
  • Attached Permissions: A Resource is similar to a file descriptor (FD) in that it has attached Permissions. In it's most basic form these are Read and Write. A Resource can have zero, one or more permissions attached to it. Restricting the type of usage of a resource is currently not possible.
  • Principle of least Privilege: Permissions for a Resource can be dropped (i.e. a permission can be removed) but can never be gained. This is similar to pledge and unveil from OpenBSD.
  • Capabilities are not opt-in: Capabilities are baked into the API itself. The Host only ever returns References and all changes have to go through these references. E.g.: The fs module. The Guest requests access to the document_dir, the Host returns a Dir reference. Dir only has relative APIs, so the guest may open files within that Dir, but not outside of it. When opening another directory within Dir, a Dir is returned again.
  • A better OS abstraction: Capabilities map better to the actual behaviour of OS'es than our current System. E.g. notifications on macOS. A user is prompted for notification consent. Upon conseting, we insert a Resource representing the UNUserNotificationCenter instance and the options we created it with (wether needed access to icon badges for example) we then return a reference to that Resource. This way each Resource and in turn each Guest only has access to the least amount of privileges required.
  • Per-guest Resource Table: The Host maintains a separate Resource Table for each Guest. This table might hold references that are shared across multiple guests. But this is ultimately up to the end user to decide. This way we archieve scoped state, something that is currently not possible. E.g. an app has a window for displaying an excel sheet and one for entering data. The data entry window only needs access to the shared excel sheet, but not to any other state of the main window (like e.g. comments). This is an important security feature our current API cannot enable.

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
@JonasKruckenberg JonasKruckenberg added type: feature request type: breaking change This issue or pull request will introduce a breaking change and requires major version bump labels Jan 19, 2023
@nothingismagick
Copy link
Member

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
Copy link
Member Author

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
Copy link
Member

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

@tweidinger
Copy link
Contributor

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 limitations1 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.

Footnotes

  1. 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
Copy link
Contributor

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.

@tweidinger tweidinger closed this as not planned Won't fix, can't repro, duplicate, stale Jul 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: breaking change This issue or pull request will introduce a breaking change and requires major version bump type: feature request
Projects
Status: 📋 Backlog
Development

No branches or pull requests

3 participants