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.
-
This may count as a zero-knowledge proof, at least from the informal description, but we are unsure. ↩