Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kittehlist docu(te)mentation

kittehlist is a todo list for kittehs (and other beings) currently in early development. kittehlist is a web application which allows you to create checklists, such as for to-do’s, groceries, or anything else that you want to keep track of in such a list. kittehlist is end-to-end encrypted, so only the people you share a list with can read that list’s content. kittehlist features realtime collaboration, and changes to the list are immediately visible to anyone looking at the list. kittehlist is effectively an upgrade and rewrite of kaufkauflist, with the intention of sunsetting kaufkauflist and its public deployment once kittehlist is a viable replacement.

This is kittehlist’s documentation (cute), for users, developers, and admins. It will eventually be publicly served so that kittehlist instances can link to it.

The documentation, just like kittehlist, is WIP. Some design details are not decided yet and are being fleshed out as development on the core featureset progresses. Basic information can be found at the project’s README.

The documentation has two sections:

  • The user documentation will contain a manual for end users. This section has not been written yet, but will be directly linked from various places in the webapp itself, as the webapp won’t contain extensive documentation.
  • The technical documentation will contain technical docs for developers, administrators, and others interested in kittehlist’s inner workings. Since the code itself contains detailed documentation, the technical docs will focus on higher-level descriptions of architecture, REST API, and cryptography. If you want to write a third-party compatible frontend or backend for kittehlist, or if you want to review or audit the cryptography, this section is suitable.

FAQ

Why is there so much frontend code? Why doesn’t kittehlist work without JavaScript?

kittehlist provides end-to-end encryption. Rendering the list on the server and sending that to the browser would defeat the whole point of kittehlist.

Why not use…

TypeScript/JavaScript with React/Angular/[insert framework here] for the frontend?

We are not web developers and we don’t like these frameworks. They make the page load slower due to both script sizes and JavaScript’s speed, and they require you to structure your application to fit the framework, often reducing accessibility by default. Even though Rust needs to go through the Wasm-JavaScript interface twice for every single web API, it’s faster than we can measure: DOM updates take almost no time, especially in comparison to network interaction. Incremental DOM updates and minimizing rendering shift are not hard problems to solve when it doesn’t need to cover all use cases. Interacting with Web APIs via web-sys is a little annoying, but gets a lot better when you write a few basic abstractions.

We actually started the frontend in TypeScript, but switched over to Rust for these main reasons. WebAssembly is already at least as fast as JavaScript and has a long way to go, as it is directly intended for native compilation. Furthermore, direct interfacing with Web APIs is on the horizon for WebAssembly, which we hope to leverage eventually.

Our WebAssembly binary is not the smallest as of right now (260KB in release mode), but we are always open to improve that situation and have already spent some effort on it. Still, it’s much easier to garbage-collect native code than JavaScript, so the code size will likely not increase very much. Already, the Web API bindings generated by wasm-bindgen are limited to every API you actually use, not all that you have enabled.

Dioxus?

From our knowledge, Dioxus requires state tracking on the server side, as well as server-side rendering. Both of these are fundamentally incompatible with end-to-end encryption, which immediately disqualifies it from being used in kittehlist.

htmx?

Htmx means server-side rendering, so it shares all of the problems of Dioxus.

Additionally, we want to write clients that do not speak HTML, including TUI or desktop clients. Therefore, we actually want to have a REST API. (Mobile clients could be solved with Dioxus without a REST API.)

My browser plugin doesn’t work on kittehlist!

This is because of kittehlist’s strict CSP, which effectively disables all JavaScript that kittehlist isn’t serving itself. It is possible to disable the CSP on the server-side, see the configuration reference. However, this opens up your client to various attacks that could compromise your list data, including XSS attacks and malicious plugins. We do not recommend disabling the CSP.

Will there be more databases supported?

Possibly. We’re focusing on PostgreSQL as it’s probably the best currently-available database that’s supported by diesel, our ORM. Diesel also supports MySQL and sqlite, so these are on the table. Databases not supported by diesel will not be considered.

What is the roadmap / release plan? When will it be stable?

No idea. This is a hobby project, and we have other interests too. However, it’s likely that we’ll start deploying kittehlist publicly as soon as it matches kaufkauflist in functionality.

Running kittehlist

kittehlist is quite easy to set up and administrate. All you need are the standalone binary, which handles all the backend tasks for you, as well as a PostgreSQL database.

Note that the administrator documentation is currently incomplete, as we bring up kittehlist’s core feature set. You should not be running kittehlist in production at the moment! However, we nevertheless welcome contributions to this documentation.

Installation and setup

Installation

kittehlist is not currently available in any package manager. You will need to build it from source.

Installing from source

  1. Check out the repo:
git clone https://codeberg.org/annaaurora/kittehlist.git
  1. Install the required prerequisites as specified in the README.
    • just is strongly recommended. In the following we will refer to the justfile recipes provided; if you don’t have just, you can instead copy and paste the commands from the justfile into your terminal.
  2. Build the frontend with either just build-frontend or just build-release-frontend. (The latter requires minify.)
  3. Build the backend with either just build-release (release build, recommended) or cargo build (debug build).
  4. Copy the artifacts to appropriate places:
    • The backend binary is at target/release/kittehlist_server (release build) or target/debug/kittehlist_server (debug build)
    • The frontend files are at target/frontend (both debug and release builds).

The development guide provides more details for most of these steps.

Setup

To set up the server, you need:

  1. A PostgreSQL database to connect to
  2. A config file pointing at the server and frontend files
  3. A reverse proxy
  4. A service file for your init system

Database

Create a PostgreSQL database for kittehlist, along with a user that has full ownership of the database. We strongly recommend the user’s permissions to be limited to this database only. See the PostgreSQL documentation on CREATE USER and CREATE DATABASE for details. For peer authentication, you may of course create a user with the same name as the user that the kittehlist server itself runs as.

No further database setup is required, kittehlist performs all setup automatically on first startup.

Config

Create a config file and point the server at it via the --config command-line argument.

The config file controls all of kittehlist’s configurable options, including the database connection and the location of the frontend files. The file format is kdl, a format similar to nginx or Caddy config files. We auto-generate full documentation of all config options, which you can read here.

Reverse proxy

We strongly recommend putting kittehlist behind a reverse proxy for TLS. Integrated TLS in kittehlist will not be supported. Additionally, you can use the reverse proxy to directly serve frontend files, increasing performance.

The reverse proxy should forward all paths to kittehlist by default. Kittehlist does not currently accept any standard proxy headers (e.g. X-Forwarded-For), but this may change in the future.

nginx

# kittehlist Nginx config file.
# If you run the reverse proxy on the same host as kittehlist using the default config,
# you only have to adjust the listening options and configure TLS correctly.
#
# This file is part of kittehlist <https://codeberg.org/annaaurora/kittehlist>
# SPDX-License-Identifier: MIT OR Apache-2.0

server {
	# Not recommended
	#listen [::]:80;

	# For TLS, strongly recommended
	listen [::]:443 ssl;
	listen 443 ssl;
	# For HTTP/3, strongly recommended
	http3 on;
	http3_hq on;
	quic_retry on;
	listen [::]:443 quic reuseport;
	listen 443 quic reuseport;

	# Set the domain name under which kittehlist is served.
	server_name kittehlist.invalid;

	# Add your TLS certificate configuration here.

	location / {
		# Adjust host and port as required.
		proxy_pass http://localhost:8062/;

		# These configuration options are required for WebSockets to work.
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";

		proxy_redirect off;
		proxy_set_header Host $host:$server_port;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Host $server_name;
		proxy_set_header X-Forwarded-Proto $scheme;
	}
}

Service file

The kittehlist server is a standard self-contained binary that will keep running forever. No external task jobs or similar are required. Obviously, the server must start after the database is ready to accept connections.

If you want to run kittehlist as a non-root user (strongly recommended), and you can’t use a dynamic user (required for postgres peer auth), you should use a non-privileged kittehlist user (without login capabilities or basically any permissions).

systemd

Note that we use (most of) the applicable security lockdown features to restrict the kittehlist process as much as possible.

# kittehlist server systemd service.
# This service configuration is designed for kittehlist’s default config.
#
# This file is part of kittehlist <https://codeberg.org/annaaurora/kittehlist>
# SPDX-License-Identifier: MIT OR Apache-2.0

[Unit]
Description=Kittehlist server
# Omit these two if it is not a dependency for your configuration.
Requires=network.target
After=network.target

[Service]
# Alternatively, use DynamicUser, but that will make PostgreSQL connections more involved
User=kittehlist

WorkingDirectory=/usr/share/kittehlist
SyslogIdentifier=kittehlist
ExecStart=/usr/bin/kittehlist_server --config /etc/kittehlist.kdl
Restart=on-failure

ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
MemoryDenyWriteExecute=true
NoExecPaths=/
ExecPaths=/usr/lib /usr/lib64 /usr/bin/kittehlist_server
ReadWritePaths=/run
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target

Config file reference

kittehlist uses the KDL format for its configuration file. Currently, the file must adhere to KDL 1.x, but 2.0 will be supported in the future.

Configuration options can be one of:

  • nested groups server { ... }
  • simple options, such as port in port 80
  • positional arguments for simple options or nested groups, like "blahblah" in static-file-dir "blahblah"
  • properties for simple options or nested groups, like database url="postgres@localhost"

Duration format

Durations can be specified in jiff’s friendly format format, so among others, any of the following are possible:

  • Single timespans with units, like 2s, 2seconds, 2 s, or 2 seconds. Note that microseconds can be written with the proper µs.
  • Timespans combined from multiple units, like 2 days 1s or 3 years 1h 3min.

Since KDL is not aware of these formats, you always have to put these durations in quotes.

server

Configuration for the web server.

Example:

server {
    bind {
        host "localhost:81"
    }
    static-file--dir "/usr/share/kittehlist/frontend"
    enable-csp true
}

bind

Where the server should listen.

You can specify IP host definitions, or Unix domain sockets (only on Unix systems, obviously). Multiple hosts and Unix domain sockets can be specified, and kittehlist will bind to all of them.

Example:

bind {
    host "localhost:80"
    host "[fd42::1]:8084"
    unix "/run/kittehlist/kittehlist"
}

host (option)

Type: Socket address; can be specified multiple times

IP host to bind to. This must have the format host:addr.

On most systems, ports below 1024 (such as the default HTTP port 80) require special permissions.

unix (option)

Type: File path; can be specified multiple times

Unix domain socket to bind to.

The socket must not be pre-existing. The directory of the socket must be pre-existing.

base (option)

Type: String

What base path/host/domain kittehlist is being exposed from.

When this option is enabled, various HTML attributes will be set appropriately. This is most important for functional Progressive WebApp (PWA) support, where the manifest must contain the correct base path.

Note that kittehlist does not support being served from anything but the top-level path. Things will break if you try to do this. Also, kittehlist does not try to match the HTTP Host header against this value, since there aren’t multiple virtual servers in kittehlist. If you need this functionality, use a reverse proxy instead.

Examples:

base "http://kittehlist.example.com"
base "https://kittehlist.example.com"
// IP addresses are also possible
base "https://[2001:0db8::1234]"
// invalid, will break in multiple ways
base "https://example.org/kittehlist"

static-file-dir (option)

Type: File path

Directory from where kittehlist will serve its static files.

enable-csp (option)

Type: Boolean (true or false)

Enable the Content Security Policy (CSP).

By default, kittehlist has a strong CSP configuration. It allows:

  • any file sourced from the same origin (fallback for all other content types)
  • WebAssembly evaluation (to run the frontend code)
  • a limited amount of inline JavaScript, verified via ‘unsafe-hashes’ SHA512 hashes:
    • various event handlers that directly call WebAssembly functions
    • the inline frontend loader script, which does the following:
      • load the WebAssembly module
      • load the wasm-bindgen JavaScript wrapper
      • attach event handler functions to the window object

If desired, this CSP can be disabled. It is strongly recommended to serve your own CSP from a reverse proxy in this case.

Note that WebAssembly evaluation via 'wasm-unsafe-eval' is currently required, as there is no standard way of starting WebAssembly without it.

workers (option)

Type: Natural number (positive integer)

How many web workers kittehlist should start per host.

This determines the maximum number of simultaneous connections that can be handled for one of the bind definitions. Therefore, the total number of workers is workers * length(bind).

The default is the number of logical cores (threads) of the system. Note that many systems can benefit from more workers than this, as worker I/O obviously doesn’t block a core from serving other requests.

database

Configuration for the database.

Example:

database url="postgres://postgres:postgres@localhost/kittehlist" {
    connection-timeout "30 seconds"
    max-connections 15
}

url (property)

Type: String

URL or connection string for the PostgreSQL database which kittelist will connect to.

The most common format for PostgreSQL URLs is postgres://user:password@host:port/database. However, any connection string accepted by libpq is allowed. See the linked documentation for additional possible options, including security options like TLS and database password files.

max-connections (option)

Type: Natural number (positive integer)

Maximum number of connections to the database.

Make sure that the database can handle this number of connections. Also, if this value is lower than the worker count, kittehlist may return errors on some requests, because not enough connections are available.

kittehlist doesn’t always open this many connections, only when necessary.

SignedDuration (option)

Type: Duration

Timeout for connecting to the database.

This timeout applies per database worker.

log

Configuration for logging.

Example:

log {
    syslog
    default-level "info"
    format "{msg} {time}"
    level "kittehlist" "trace"
    level "actix_server" "debug"
}

syslog

Specify this option to log to syslog directly. This disables standard output logging and makes the format option ineffective.

This tries to connect to syslog:

  • by Unix socket /dev/log or /var/run/syslog (in this order)
  • by TCP connection to localhost:601
  • by UDP connection to localhost:514

Examples:

syslog
syslog application_name="not-kittehlist"

application-name (positional argument)

Type: String

Application name that kittehlist will send. Default is “kittehlist”.

default-level (option)

Type:

Default logging level.

This level only applies to modules that are not configured with a separate level option.

The default is info. It is generally not recommended to set this to debug or trace unless you are debugging third-party dependencies and/or have been asked to do so as part of a bug report.

format (option)

Type: String

Log format.

kittehlist uses a custom log format that supports {placeholders}. Even though this looks like the Rust or Python format string syntax, none of the formatting options from them is currently supported. kittehlist uses some custom options for certain formats, which are separated by a : from the main placeholder. Multiple options each use their own :. If you want to include literal curly braces, use {{ and }}.

Supported placeholders:

  • {level}: Prints the log level as capitalized colored text, always 5 characters wide, left-aligned. You can customize the printout by adding one of these options:
    • no_align to remove the default alignment.
    • no_color to remove the color.
  • {message}: Prints the log message itself.
  • {time}: Prints the current time as an ISO 8601 timestamp of the current UTC time with seconds precision. Use the local option to use the local time zone instead.
  • {module}: Prints the module that the log message originates from.

The default format is unspecified and may change at any time. If you need to rely on a certain log format, set this option.

level (option)

Type: Two positional arguments, module and level; can be specified multiple times

Change log levels per module.

This option can be specified multiple times as level <module> <log level>. Each directive changes the log level of a different module.

Examples:

level "kittehlist" "trace"
level "actix_server" "info"
level "actix_server::middleware" "error"

Useful modules includes:

  • kittehlist (this is mainly what you want)
  • kittehlist_api (related to kittehlist’s data model and cryptography)
  • actix_server (web server operation)
  • actix_web::middleware::logger (logs requests nginx-style)

Development

Prerequisites

  • Rust and Cargo for frontend and backend (recommended: via Rustup)
  • A PostgreSQL database for kittehlist to connect to
  • wasm-pack for building the Rust frontend
  • esbuild for bundling CSS and static files
  • (optional) diesel (ORM) which must be configured for a PostgreSQL database
  • (optional) minify for minifying frontend release artifacts
  • (optional) pre-commit for pre-commit checks that cover the most basic CI checks; install hooks with pre-commit install
  • (optional) just script runner

Basic workflow

kittehlist’s backend is a single Rust application (cargo build --bin server).

kittehlist’s frontend is a Rust WebAssembly module in addition to some static files which the server reads from the file system. The static files are bundled and minified with esbuild, while the Wasm module is built with wasm-pack. See just build-frontend for details.

The justfile provides many more utility functions, many of which are not documented elsewhere. Use just --list to view all of them and their descriptions.

For NixOS, a shell.nix development environment is provided.

You can put custom Cargo configuration options in .cargo/local.toml. This file is automatically included if it exists. Some useful unstable options are commented out in .cargo/config.toml and should be copied to local.toml before use.

kittehlist compiles for and has been tested/deployed on AArch64 and RISC-V (specifically, aarch64-unknown-linux-gnu and riscv64gc-unknown-linux-gnu). The configuration in .cargo/cross-compile.toml is set up for cross-compilation from an x86 Linux machine (tested on Ubuntu, Debian, and Arch Linux). If you want to compile for either of these architectures natively, comment out the relevant options.

Testing

By convention, dev_config.kdl is the kittehlist server configuration used for development. This file is gitignored so you can’t commit it accidentally, and it’s also what the just recipes use.

kittehlist contains plenty of tests. Use cargo test or cargo nextest run to run them. Note that on first run, due to database migrations, the multi-process cargo nextest may fail. Either use cargo nextest --jobs 1 or try re-running it.

The integration tests in the top-level tests folder will run the server in a special test mode and execute regular API calls against it. Note that these calls will of course interact with the database, and the database will be permanently changed by these tests. You should of course never use a production database for development and testing. The config used by integration tests is tests/test_config.kdl. (The tests will not compile if you don’t create this file.)

Commits and checks

We use lefthook as a pre-commit system. Use lefthook install to install the hooks.

Use reasonable descriptive commit messages. We don’t like Conventional commits (see here and here), since they are the opposite of descriptive, so do not use them.

We run the usual suite of tests, lints and other checks on CI. This includes cross-compilation to AArch64 and RISC-V 64GC. As we have experienced CI runner issues with the latter two jobs in the past, we don’t necessarily require these checks to pass for each commit.

Releasing and Packaging

This section concerns itself with how to build kittehlist release artifacts, and how to configure it for release. Note that at present, kittehlist is not in a state where it can reasonably be packaged in a sensible way.

Currently, we are not accepting contributions to add package definitions into the main kittehlist repo. We are however open to contributions which allow kittehlist to be packaged (better) on some system, including ones that don’t currently work, and to link any kittehlist package from the repo.

The Nix shell can be used as a starting point for packaging on NixOS.

Note that in addition to MIT OR Apache, the license for the Inter font used and vendored by kittehlist must be observed, which is the SIL Open Font License 1.1. All three licenses are OSI-compatible.

When building kittehlist repeatedly, build information may be cached even when some build inputs change. This is to speed up development builds where this does not matter, but it may result in kittehlist_server --version containing incorrect version or feature information (among others). To ensure that all build information is correct, delete the build directory before performing a release build.

Release Profile

Due to a long-standing limitation in wasm-opt (see also frontend below), it is not possible to use the built-in release profile for anything other than the frontend build. Therefore, the ideal release build settings have been applied to the releash profile:

$ cargo build --profile releash

Dynamic Dependencies

By default, kittehlist comes with no dynamic dependencies to simplify development and CI. This can be changed by disabling the feature no_dynamic_deps, which is on by default:

$ cargo build --no-default-features

When no_dynamic_deps is present, the PostgreSQL backend has no TLS support, since that always requires a dynamically linked OpenSSL. When this feature is missing, kittehlist requires the following dynamic libraries (and their dependencies):

Frontend

The kittehlist frontend is put together from:

  • static files copied by esbuild
  • CSS bundled by esbuild
  • WebAssembly modules and their JavaScript interface shims created by wasm-pack and wasm-bindgen

The build-release-frontend recipe additionally minifies all plaintext files using minify. This is optional but reduces the file transfer size.

It is crucial to use build-release-frontend (or directly wasm-pack with --release) for the WebAssembly module. For development purposes, we add DWARF information and don’t run wasm-opt for the debug build, which massively increases the binary size. The release build decreases the WebAssembly size from multiple megabytes down to ~300KB.

Additionally, release builds should disable the default debug_log feature and enable the disable_log feature. (The reason for two separate features is unfortunate, but rooted in how Cargo features and defaults work.) This removes both regular and panic logging, which saves 20KB+ of space. You may also consider specifying RUSTFLAGS="-C panic=abort", though in our experience it does not help much.

Paths

kittehlist does not have a built-in default for the config file. On Unix systems that run one kittehlist installation, /etc/kittehlist.kdl should be used.

On Unix systems, it is recommended to install the frontend to /usr/share/kittehlist/frontend. The default-provided configuration should reference this directory.

At the moment, kittehlist does not require write access to any paths, as all data is stored with the database.

Architecture

Thanks to the end-to-end encryption, kittehlist provides a large amount of functionality in the frontend. The backend necessarily gets relegated to a database mediator role unlike in other applications:

  • authentication and access control for lists
  • live list updates between all connected clients

Web server path system

  • Static files: All paths under /static.
  • REST API: All paths under /api. The API is versioned and the current version is found at /api/v1. (It is unlikely that we will support more than one API version at a time, but this prevents clients from accidentally misusing an API.)
  • User-facing paths: Any other path; basically all of them serve HTML.

Frontend-backend-relationship

Frontend and backend communicate via a REST API. This API is intended to represent the whole kittehlist functionality, such that alternative frontends can be written against it. Some web pages directly perform backend logic on visit (e.g. /list/new which immediately creates a new list and redirects to it), others are largely static (e.g. any other list page, which serves the exact same HTML, all list-specific logic is implemented in the frontend which reads the URL).

On the HTTP server, the entire frontend lives under /static, with the exception of the special site.webmanifest file (used by convention and required by some PWA implementations). kittehlist will serve its own static files from the filesystem here.

Because the /static path is reserved for static frontend files, these files can instead be served by a CDN, load balancer, or reverse proxy, all with caching as usual for rarely-changing static files. This use case is explicitly supported and any issues with it will be fixed. While kittehlist does support caching, its overall static file implementation is probably less performant than tailor-made solutions.

APIs

Note: This documentation is preliminary and requires more formalization. An OpenAPI document will eventually be provided.

All REST endpoints are provided under /api/v1.

Common datatypes

UUIDs are represented with their canonical form in a string. Serial numbers are represented as a decimal number, optionally in a string (to avoid issues with JavaScript Number accuracies).

Binary data is represented in Base64 wherever plausible (except UUIDs).

A list entry is represented as:

{
	"id": "Serial number for the entry ID",
	"ciphertext": "Base64-encoded ciphertext of the list entry",
	"nonce": "Base64-encoded Nonce for encryption of the ciphertext"
}

/user/login

APIs related to user login. Rate limited to ~5 requests per endpoint, username, and minute.

GET /user/login/salt?username=<username>

Request the salt for a user account. Yields 403 if the user does not exist (or for rate limiting, to not leak information).

{
	"salt": "<base64 encoded salt>"
}

GET /user/login

Requests a session token for a user account. The Authorization header must contain the username and HA password hash. Unsuccessful authentications return 403 (whether user exists or not).

{
	"session-token": "<base64 encoded token>"
}

The access token is added to the site cookies by JavaScript (using the Set-Cookie header is unnecessarily convoluted thanks to CORS).

/user/register

Create a new user account. (TODO)

DELETE /user

Delete a user account. (TODO)

/user/list

Manage a user’s saved lists. Get all lists (GET), which returns a JSON array.

A user’s lists are represented as:

{
	"id": "<UUID of the list>",
	"elk": "<base64 encrypted list key>",
	"token": "<access token UUID if present>"
}

For single lists (/user/list/<list uuid>): get a list (GET), add a list (POST), delete a list (DELETE).

/list/<list uuid>

Retrieve a list’s data in JSON form (GET), update a list’s data (POST), delete a list (DELETE).

The response to the GET request is:

{
	"id": "<list id>",
	"title": "<base64 encoded encrypted list title, optional>",
	"nonce": "<base64 encoded title nonce, optional>",
	"entries": [
		// <array of all items>
	]
}

An update (POST) is presented in the following form:

{
    "title": "<base64 encoded, encrypted new or changed list title>",
    "nonce": "<title nonce, required if title is changed or now>",
    "add": [
        // <items that should be added, without IDs, so only ciphertext-nonce pairs>
    ...],
    "modify": [
        // <items that should be modified>
    ...],
    "delete": ["<list of deleted ids>", ...]
}

Any top-level key may be omitted to not submit updates of this type.

/list/<list uuid>/permissions

Manage permissions for a list. (TODO)

POST /list/new

Create a new list, no body required. The server responds with:

{
	"id": "<uuid of the new list>"
}

Cryptography and Security

Parts of this document are WIP, and not all parts are as rigorously specified as they should be.

The cryptography in kittehlist solves only one problem: end-to-end encryption (E2EE) of list contents.

Threat model

kittehlist follows the cryptpad threat model. It is generally assumed that your kittehlist instance is unmodified and that others who have sufficient access to a list don’t share the access credentials with illegitimate actors.

Under these assumptions, kittehlist tries to guarantee:

  • that noone of these can read the text stored in your list:
    • any other user without legitimate access (link sharing, user account access) to the list
    • the server administrator with full database access
    • any powerful attacker with full control over the network between you and the server
  • that you have control over who has which levels of access for a certain list, but only if you have these permissions for the list.

The cryptography in kittehlist is based on state-of-the-art algorithms and protocols, and their implementation is taken from trusted and audited libraries like the RustCrypto implementations. Should a flaw in kittehlist’s protocols be uncovered, we will update it and provide no backwards compatibility.

All server-client communications should happen over HTTPS, meaning TLS guarantees that no attacker can inject, read, or alter messages sent between you and the server. No TLS functionality is planned for kittehlist itself, meaning that any public instance should run behind a reverse proxy that provides this functionality. The highest possible TLS versions (1.3 at the time of writing) are strongly recommended, to avoid vulnerabilities from older protocols and primitives.

Primitives

kittehlist relies on the following cryptographic primitives:

  • Security during client-server communication: TLSv1.3 is strongly recommended, but kittehlist does not implement this itself and there are no plans to do so in the future. You should put kittehlist behind a reverse proxy that provides TLS.
  • Authenticated Encryption with Associated Data (AEAD): ChaCha20Poly1305
  • Password-based key derivation (PKDF): argon2id. We follow the RFC recommendation for “less available memory” (as the PKDF runs in the frontend) and select t=3 iterations, p=4 lanes, m=2^(16) (64 MiB of RAM), 128-bit salt, and 256-bit tag size.
  • Message Authentication Code (MAC): HMAC with BLAKE3 as a hash function.

The latter three primitives will be notated as AEAD, PKDF, and `MAC, respectively.

User accounts

User accounts are used for storing user’s lists and list encryption keys with the server (securely) so that they cannot be lost by the browser storage being cleared. Because the user’s password is used for both server authentication and encrypting list keys, the password itself cannot ever be seen (or be derivable) by the server. Therefore, we calculate the hashed password authenticator HA as:

send (username) to server and receive (random-data)
salt = "user-login-" + random-data
HA = PKDF(P=password, S=salt)

HA is passed to the server, there stored with PKDF(P=HA, S='\0').

Usernames are restricted to any unicode printable character. They are required to be unique, so that adding a user to a list can be done via the name in the first place.

Users obtain session tokens when they log in. There is a time limit after which a session token becomes invalid when it hasn’t been used. The server redirects to a login page if the session token has expired, and JSON APIs return 403.

Encryption

Each list is encrypted with a key that is stored in the list’s fragment. List keys are used for symmetric encryption, so they are generated by a CSRNG. The list keys are encrypted when stored on the server. First, a password hash HL is created from the password using PKDF and list-<list UUID> as salt. Then, authenticated encryption with AEAD is performed with this as the key and the list key as the plaintext. The encrypted list key ELK and the random nonce is sent to the server for storage.

Once a client has first derived a list’s key, it stores that key in localStorage. It can read such a pre-derived key either from the fragment (especially for lists newly shared with the user) or from the localStorage (especially for lists the user previously visited). The advantage of using localStorage per default is so that the key is not easily visible in the fragment to an outsider. Once a new list is visited, the key in the fragment is also copied to localStorage for future use.

Encryption of lists happens per item. AEAD is used with a random nonce per item (stored server-side), which also provides authentication.

To create a list:
receive (uuid) from server
lk = CSRNG()
store (uuid, lk)

To encrypt a list item, or list metadata:
plain = item (or metadata) user input text
nonce = CSRNG()
uuid, lk = from storage
cipher = AEAD_encrypt(plain, nonce, lk)
send (cipher, nonce) to server

To decrypt a list item or list metadata:
receive (cipher, nonce) from server
uuid, lk = from storage
plain, authenticated = AEAD_decrypt(cipher, nonce, lk)
if not authenticated:
    show warning to user
    discard list item or metadata

To store a list key with the server:
password = from user input
uuid, lk = from storage
salt = "list-" + string(uuid)
hl = PKDF(P=password, S=salt)
nonce = CSRNG()
elk = AEAD_encrypt(lk, nonce, hl)
send (uuid, elk, nonce) to server

To receive a list key from the server:
receive (uuid, elk, nonce) from server
password = from user input
salt = "list-" + string(uuid)
hl = PKDF(P=password, S=salt)
lk = AEAD_decrypt(elk, nonce, hl)
if not authenticated:
    show warning to user
    discard list key
else:
    store (uuid, lk)

Encryption granularity and metadata leakage

To obscure the changes that users are performing on a list, each item’s full contents, stored in JSON format, are encrypted before transmission. Also, items can be set to be “invalid”, meaning that they effectively do not exist and all of their data is ignored. This means that the server cannot differentiate between an item being added, modified or removed, as long as the change only affects an already-existing entry.

In general, the list will have many item entries that are not valid. To add an item, the invalid item with the lowest ID is set to valid and filled with the appropriate data. The use of the lowest ID makes the operation deterministic and look more like a normal modification, since those are not random either. Similarly, to remove an item, the item is set to invalid, and the item data can optionally be cleared (though this is not recommended, since that leaks metadata via ciphertext length).

There are edge cases where either there are no more invalid list items available, or where so many invalid list items are available that the list is unnecessarily large. In that case, list items can be added and removed, but the client should do this in batches to obscure the data. The batch limits are as follows:

  • When no more invalid items are available, a batch of 128 items are created, all set to invalid initially.
  • When more than 192 invalid items are present, delete 128 random invalid items, leaving 64 invalid items.

Authentication

Secret-key knowledge authentication

Scenarios possible without this authentication: Even if a user has access to a list, this may not mean that they also have the correct key to encrypt and decrypt items on that list. In a malicious setting, a list without access permissions whose ID (but not key) is leaked can always be fully deleted or arbitrarily overwritten by an attacker. In a non-malicious setting, frontends may accidentally overwrite list data using a newly-generated or incorrectly used key.

To protect against both scenarios, kittehlist provides a mechanism to prove knowledge of the key to other key holders as well as the server (who is not a key holder), without leaking any information about the key.1

A list contains a challenge string C which is provided to all clients unconditionally. It also contains a corresponding challenge hash field CH. CH is never sent to clients and used for server-side checks described below. Both fields are initially empty.

Any client may instruct the server to set CH and C to arbitrary well-formed strings, provided that they have not yet been set. The proper mechanism for this is as follows:

generate or retrieve lk for list uuid
c = CSRNG()
ch = MAC(K=lk, M=c)
send (c, ch) to server

Any request which wishes to update any part of the list must include CH. Update request are only processed by the server if the received CH matches the CH previously stored in the list. Otherwise, the modification is rejected.

Clients can instruct the server to rotate C by providing the new C, old CH, and new CH in one message.


each access is assigned a set of permissions on the list

when creating a list one should be able to opt not to create access tokens or link user accounts at all so that a user has all permissions if they know the list’s id and the encryption key.

JavaScript does all authentication with an api separate from the frontend webserver. It uses either the user account credentials or an access token.

Incase of a user account, the API will send back a cookie upon successful authentication so that users with a user account don’t have to enter credentials multiple times. The cookie should be stored for the API path, not the HTML/CSS/JS path and therefore only used by the JavaScript. Cookies are used because they have better access control in the browser’s storage than localStorage.

HTTP authentication with user account:

Authorization: Basic username=<username utf-8 base64>, password=<password hash HA base64>

Permissions

  • View items
  • Add item
  • Delete item, delete list, edit authentications
  • Check, uncheck items

“Add item” is separate from “Delete item” and “Check” item because some use cases might require this. E.g. a list where you can suggest songs to be played by adding items. You should not be able to delete other’s suggestions and songs might only be checked by someone in charge of the music when it was decided that they will be played.


  1. This may count as a zero-knowledge proof, at least from the informal description, but we are unsure. ↩