Accounts and devices

Root secret and DID

A Tinline account starts with 256 random bits called the root secret. It can be exported as a BIP39 recovery phrase or as a one-line checksummed tlkey-… recovery key. These are two representations of the same account secret, not separate credentials.

The BIP39 seed is passed through HKDF-SHA256 with context osv/identity/signing/v1 to derive an Ed25519 account key. The public key is represented as did:key:z…, including the Ed25519 multicodec prefix before base58 encoding. The DID is self-authenticating: verification needs no account registry or resolver.

The root secret is the identity. Anyone retaining it can sign as the account. Tinline therefore does not claim cryptographic revocation of a hostile linked device.

Account key, install and endpoint key

Tinline separates three identifiers:

The account signs an attestation binding its DID to the current device key. Application admission checks that attestation, confirms the attested key is the QUIC peer, checks the expected DID and requires the appropriate token or relationship.

Content authorship is signed by the account key. Device keys authenticate connections and transport batches; they are not the long-term author identity.

Endpoint rotation

Private and hosting-group endpoint rotation is implemented and crash-safe, but the default policy in version 0.1.38 is disabled. The apps expose neither an enable switch nor a lost-touch screen. When a caller explicitly enables the core policy, its standard schedule is seven days plus up to one day of persisted jitter, with a seven-day announcement period. A host can then rotate its group endpoint too; an ordinary group member does not.

The old endpoint key signs a succession link to the new one. A contact can preserve verification when a valid chain reaches a key it already knew. An installation retains its latest four links; a device-set change that cannot be connected through those links is treated as an unlinked change and resets verification.

An enabled rotation changes neither the DID nor the install ID, writer identity, grants or conversations. Blocking, contact removal, group removal and host revocation do not trigger or reschedule it. If a contact misses an enabled announcement window, the core records lost_touch; app controls and recovery UI for that state are not built.

Accounts on one installation

One installation may hold several accounts, but only the selected account is unlocked and online. Switching accounts closes its endpoints, drops unlocked secrets and opens the selected account; switching is refused during a call. Other accounts do not ring on that device.

The account directory contains a readable profile and sealed databases. The exact rows evolve, but the security boundary is:

data readable while locked?
Display name, DID and device public key yes, deliberately, for account selection and lock UI
Root secret and device private key no; sealed in the vault
Contacts, tokens, blocks, call log and sync state no; sealed records
Chats, groups, receipts and local search data no; sealed records
Attachment and history contents no; independently encrypted blobs
Directory names, record paths, activity days, sizes and blob hashes yes, as storage-layout metadata

Vault

The root secret and current device private key are sealed with AES-256-GCM under a random data key (DEK). When a passphrase is set, Argon2id derives a wrapping key using stored parameters (normally 64 MiB, three iterations and four lanes), and the DEK is wrapped under it. Parameters outside the accepted safety range are rejected rather than silently weakened.

A passphrase is optional. Android can remember the DEK using Android Keystore. Desktop remembering is optional and requires an available secure OS keyring; otherwise a passphrase is required. Creating or restoring a no-passphrase account is not committed to disk until the platform has successfully saved and read back its remembered key.

Revealing the root secret or changing the passphrase requires the passphrase when one is set. A device that is already unlocked can still act as the account.

Sealed stores and blobs

Account, chat, group and search stores use separate keys derived from the root secret and their store context. Values are AES-256-GCM sealed and bound to their record paths, so a ciphertext row cannot be moved to a different key and remain valid. Loro documents, postcard records and JSON profile values all exist inside this storage model; there is not one universal encoding.

Files use fresh random AES-256-GCM keys and 64 KiB encryption chunks. Ciphertext is addressed by its BLAKE3 hash and transferred through iroh-blobs, whose own 16 KiB chunks support resumable transfer. Encryption chunking and transfer resumption are separate layers.

Stores use long-lived per-store keys with random nonces rather than a ratchet. There is no forward secrecy for copied local data. Filesystems and flash translation layers also prevent a reliable secure-deletion promise.

Linking versus succession

Linking copies the root secret to another device. Unlinking removes that installation from normal sync and dialing, but cannot stop a device that retained the secret from creating a fresh key and signing as the account.

A separate account-identity succession design would let the old account key name a replacement DID and let contacts follow it operationally. That is not implemented. Old signatures would remain mathematically valid, and competing succession statements still require a conflict policy.

Found something wrong? Report it on GitHub ↗