Summary
- Only published, standard algorithms. Everything the app encrypts uses algorithms published by NIST or the IETF: AES-256-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, Argon2id, PBKDF2, Ed25519 and the SSH and TLS protocols. Nothing is proprietary or secret. The list, with its standards.
- The algorithms are fixed. The app has no setting for choosing ciphers and no way to plug in other ones.
- Encrypted files don't rely on a secret format. This page describes each format exactly, so anyone can check what the app does. Their security rests on your passphrase, or for an invitation on a one-time code, alone.
Where the app uses cryptography
| What | Algorithms |
|---|---|
| SSH and SFTP connections to your servers, including port forwarding and jump hosts | The SSH protocol. Ciphers aes256-gcm, aes128-gcm, chacha20-poly1305, aes256-ctr and aes128-ctr; key exchange curve25519-sha256, ECDH over NIST P-521, P-384 and P-256, and Diffie-Hellman (group exchange and group 14, both with SHA-256); host keys Ed25519, RSA with SHA-512 or SHA-256, and ECDSA; MACs HMAC-SHA-256 and HMAC-SHA-512 (encrypt-then-MAC preferred), and HMAC-SHA-1. The app and your server negotiate from this fixed list. |
| SSH keys | Keys the app generates are Ed25519 (the default), ECDSA over NIST P-256, P-384 or P-521, or RSA with 3072 or 4096 bits. Keys you import are read in the standard OpenSSH and PEM private-key formats, including passphrase-protected ones. |
| Checking a server's host key | SHA-256 fingerprints. On the Mac, hashed names in your ~/.ssh/known_hosts are matched with HMAC-SHA-1, as OpenSSH writes them. |
| HTTPS connections (Proxmox, a WebDAV sync server, a server's BMC, the optional assistant) | TLS, through the BoringSSL library built into the Dart runtime. A certificate your device doesn't trust is accepted for a sync server or a BMC only if its SHA-256 fingerprint matches one you chose to trust. |
| Web admin pages opened in the app's web view | Your device's own TLS (WebKit on Apple platforms, the Android System WebView on Android), inside the SSH tunnel. A certificate your device doesn't trust is accepted only if its SHA-256 fingerprint matches one you chose to trust. |
| Passwords and SSH keys stored on your device, alert destinations, and saved configuration-drift snapshots | AES-256-CBC with PKCS#7 padding and a random IV for each entry, under a random 256-bit key that is kept in the Keychain (Apple platforms) or the Keystore (Android) |
| Other secrets on your device (sync's keys and server password, BMC logins, backup repository passwords, the assistant's API key) | Kept in the Keychain or the Keystore directly |
| Encrypted backups and "Send credentials" files | Argon2id and AES-256-GCM (format below). Older files: PBKDF2-HMAC-SHA256 and AES-256-GCM (below) |
| Sync | Argon2id, HKDF-SHA256, HMAC-SHA256 and AES-256-GCM (format below) |
| App lock | Your device's own Face ID, Touch ID, fingerprint or passcode check |
Backup and credential files (SSKB2)
Settings → Backup & Restore and Credentials → Send credentials both write this format. A backup's file name ends in .sskb, a credential file's in .sskc. The file holds one JSON document, encrypted with a key derived from a passphrase you choose. The passphrase must be at least 8 characters, and the app never stores it.
All numbers are unsigned and big-endian. The offsets are those of a file the app writes today, with a 16-byte salt.
| Offset | Bytes | Field |
|---|---|---|
| 0 | 5 | Magic: the ASCII text SSKB2 |
| 5 | 1 | Key derivation ID: 1 means Argon2id |
| 6 | 1 | Length of the key derivation's settings: 12 for Argon2id |
| 7 | 4 | Argon2id memory, in KiB (65536, which is 64 MiB) |
| 11 | 4 | Argon2id passes (3) |
| 15 | 4 | Argon2id lanes (4) |
| 19 | 1 | Salt length (16) |
| 20 | 16 | Salt, random for every file |
| 36 | 1 | Cipher ID: 1 means AES-256-GCM |
| 37 | 12 | AES-GCM nonce, random for every file |
| 49 | rest − 16 | Ciphertext |
| end − 16 | 16 | AES-GCM authentication tag |
- Key: Argon2id (RFC 9106, version 0x13) with the memory, passes and lanes in the header (64 MiB, 3 and 4 today, RFC 9106's second recommended setting), the header's salt, and a 32-byte output. The password is the UTF-8 encoding of your passphrase after Unicode NFC normalization, so the same passphrase typed on different keyboards gives the same key. There is no secret value or associated data. This is the same derivation sync uses for its keyring.
- Cipher: AES-256-GCM (NIST SP 800-38D) with a 12-byte nonce and a 16-byte tag. The associated data is the whole header, every byte before the ciphertext (49 bytes today), so no header byte can be changed without the tag failing.
- What's inside: UTF-8 JSON. A backup is an object with
"kind": "full_backup","version": 1, the time it was made (exportedAt), and yourcredentials,groups,hosts,portForwards,snippetsandalertDestinations. A credential file has"kind": "credentials","version": 1,exportedAtand only thecredentialsyou picked, each with its name, username, type, password, private key, key passphrase and privilege setting. - Limits when opening a file: the app opens a file only if its settings are within these bounds: memory at most 256 MiB and at least 8 KiB per lane, 1 to 16 passes, 1 to 16 lanes, and a salt of 16 to 64 bytes. A file with an unknown key derivation or cipher ID, or with settings above those bounds, is reported as made by a newer version and not opened. So the settings can be raised later without a new format.
A wrong passphrase and a damaged or altered file both fail the authentication tag, and the app can't tell them apart. Nothing is restored from a file that fails.
Versions of the app from before SSKB2 can't open SSKB2 files. They report that the file doesn't look like a backup, and change nothing.
Older backup files (SSKB1)
The app still opens SSKB1 files, and always will, but no longer writes them. Earlier versions wrote backups and credential files in this format. The JSON inside is the same as above.
| Bytes | Field |
|---|---|
| 5 | Magic: the ASCII text SSKB1 |
| 16 | Salt, random for every file |
| 12 | AES-GCM nonce, random for every file |
| 16 | AES-GCM authentication tag |
| rest | Ciphertext |
- Key: PBKDF2 with HMAC-SHA256 (RFC 8018), 210,000 iterations, a 32-byte output. Its inputs are the salt and the UTF-8 bytes of the passphrase exactly as typed, without normalization, as those files were made.
- Cipher: AES-256-GCM, with no associated data. Note that the tag comes before the ciphertext in this format.
Sync
Sync keeps your hosts and related records the same on your devices, through a WebDAV server you choose. (The privacy policy lists what syncs.) The server only ever receives encrypted files, plus one unencrypted file that holds nothing secret. The sync folder looks like this:
server-sidekick-sync/
keyring.json the data key, encrypted with your passphrase
devices/<deviceId>.ssks one encrypted file per device
Setting up sync creates a random 256-bit data key and a random 128-bit key ID for it. Every device file is encrypted under a key derived from the data key. Each device keeps the data key in its secure storage, and it is stored on the server only after being encrypted with a key derived from your sync passphrase. The sync passphrase must be at least 15 characters, counted as Unicode code points after NFC normalization. It never leaves your devices and is not stored.
The keyring (keyring.json)
A JSON object. Byte strings are standard Base64 with padding.
{
"format": 1,
"keyId": "<16 bytes>",
"kdf": {"alg": "argon2id", "m": 65536, "t": 3, "p": 4, "salt": "<16 bytes>"},
"wrappedKey": "<nonce (12) | ciphertext (32) | tag (16)>"
}
- Key-encryption key: Argon2id (RFC 9106, version 0x13). The defaults are 64 MiB of memory (
m, in KiB), 3 passes (t) and 4 lanes (p), RFC 9106's second recommended setting. It uses the 16-byte salt and makes a 32-byte output, with no secret value or associated data. The password is the UTF-8 encoding of your passphrase after Unicode NFC normalization. The settings are stored in the file, so they can be raised later without changing the format. A device uses a keyring only if its settings are within these bounds: memory at most 1 GiB and at least 8 KiB per lane, 1 to 16 passes, and 1 to 16 lanes. - wrappedKey: the data key, encrypted with AES-256-GCM under the key-encryption key and a random 12-byte nonce. The associated data is the ASCII text
ssk-sync-keyring-v1followed by the 16 bytes of the key ID. - While a key is being replaced (Settings → Sync → Rotate key), the keyring briefly also has
"previous": {"keyId", "wrappedKey"}. That is the old data key, wrapped under the same key-encryption key with the associated datassk-sync-keyring-previous-v1| the old key ID. Every file on the server stays readable until each device has re-encrypted its own. - After a key has been replaced, the keyring can have
"replaced": {"keyIds": [...], "mac": "<32 bytes>"}. That is up to 8 earlier key IDs, newest first. Your other devices use it to recognise the new key as a replacement and carry on without setting up again.macis HMAC-SHA256 overssk-sync-keyring-replaced-v1| key ID | each listed key ID in order. Its key is HKDF-SHA256 (RFC 5869) of the data key, with no salt (RFC 5869's default), the info stringssk-sync-keyring-replaced-v1and 32 bytes of output. Only a device that unlocked the data key with your passphrase can check or write that list.
Device files (SSKS1)
Each device writes only its own file, devices/<deviceId>.ssks. The app makes each device ID a random UUID (version 4); a reader accepts any ID of 1 to 64 characters from A–Z a–z 0–9 _ -.
| Bytes | Field |
|---|---|
| 5 | Magic: the ASCII text SSKS1 |
| 16 | Key ID of the data key the file is encrypted under |
| 32 | File salt, random for every write |
| 12 | AES-GCM nonce, random for every write |
| rest − 16 | Ciphertext |
| 16 | AES-GCM authentication tag |
- File key: HKDF-SHA256 (RFC 5869) of the data key, with the file salt, the info string
ssk-sync-device-file-v1and 32 bytes of output. Every write therefore has its own key. - Cipher: AES-256-GCM. The associated data is
SSKS1| key ID | the UTF-8 device ID taken from the file's name. A file copied or renamed to another device's name fails to decrypt, and so does one encrypted under another key. - Plaintext and padding: the payload's length as a 4-byte big-endian number, then the payload, then zero bytes. The zero bytes make the whole file a power of two in size, at least 4 KiB, so its size reveals only roughly how much you sync. The file's 81 bytes of header and tag are counted in that size. Files over 64 MiB are neither written nor read, and a reader rejects padding that isn't all zeros.
- Payload: a UTF-8 JSON object with the format version (
"format": [1, 1]), the device ID (device), a write counter (seq), the writer's clock in milliseconds (clock) and the synced records (records). It can also have the device's display name (name): the device model, or a name you gave it in Settings → Sync, never your device's own name. Because the name sits inside the encrypted payload, the server never sees it.
A torn or tampered file fails its authentication tag. It is skipped, never partly applied. A file whose write counter is lower than one already seen from that device is ignored as a rollback.
Adding a device with a code (SSKI1)
A device that already syncs can show a QR code (Settings → Sync → Add a device) that lets a new device join without typing the sync passphrase. The code holds a one-time secret; the data key goes to the server encrypted under a key derived from that secret, so neither the code nor the server's copy opens anything on its own.
The code is this text, as a QR code or copied, with each value URL-encoded:
server-sidekick-sync:join?v=1&url=<sync folder address>&user=<username>&cert=<certificate fingerprint>&id=<invitation ID>&secret=<secret>
idis 16 random bytes andsecretis 32 random bytes, each written in URL-safe Base64 without padding (22 and 43 characters).certis present only if the inviting device trusts the server's certificate by its fingerprint: the certificate's SHA-256 as 64 hex digits.- The code never holds the sync passphrase, the server's password or the data key.
The invitation file is invites/<id>.sski in the sync folder:
| Bytes | Field |
|---|---|
| 5 | Magic: the ASCII text SSKI1 |
| 12 | AES-GCM nonce, random |
| rest − 16 | Ciphertext |
| 16 | AES-GCM authentication tag |
- Key: HKDF-SHA256 (RFC 5869) of the code's 32-byte secret, with the invitation ID's 22 characters (as ASCII text, not the decoded bytes) as the salt, the info string
ssk-sync-invite-v1and 32 bytes of output. - Cipher: AES-256-GCM. The associated data is
SSKI1| the invitation ID's 22 characters, so a file renamed to another ID fails to decrypt. - Plaintext: UTF-8 JSON:
{"v": 1, "keyId": "<16 bytes>", "key": "<32 bytes>", "expires": <milliseconds since 1970>}, with the data key and its ID in standard Base64. - Lifetime: an invitation expires 10 minutes after it is made, by the inviting device's clock; the new device allows up to 5 minutes more for clocks that disagree. The new device deletes the file before it uses the key, and the inviting device deletes it when its dialog closes or once it has expired.
Standards and libraries
The app uses only these algorithms, each as its standard defines it. All of them run in code built into the app.
| Algorithm | Standard | Where, and which library |
|---|---|---|
| AES-256-GCM | FIPS 197 (AES); NIST SP 800-38D (GCM) | Backups, sync, invitations: cryptography 2.9.0. SSH: dartssh2 4.1.0 |
| Argon2id, version 0x13 | RFC 9106 | Backups and the sync keyring: cryptography 2.9.0 |
| HKDF-SHA256 | RFC 5869 | Sync file keys, the keyring's key list, invitations: cryptography 2.9.0 |
| HMAC-SHA256, HMAC-SHA512 | RFC 2104 and FIPS 198-1; SHA-2 per FIPS 180-4 | The keyring's key list: cryptography 2.9.0. SSH: dartssh2 4.1.0. Hashes of synced values kept on the device: crypto 3.0.7 |
| SHA-256, HMAC-SHA-1 | FIPS 180-4; RFC 2104 | Host-key and certificate fingerprints, OpenSSH's hashed host names, SSH: crypto 3.0.7, dartssh2 4.1.0 |
| PBKDF2-HMAC-SHA256 | RFC 8018 | Opening older SSKB1 backups only: cryptography 2.9.0 |
| AES-256-CBC, PKCS#7 padding | FIPS 197; NIST SP 800-38A (CBC); RFC 5652 (padding) | Passwords and keys stored on the device: hive_ce 2.20.0 |
| AES-CTR, ChaCha20-Poly1305 | NIST SP 800-38A; RFC 8439 | SSH: dartssh2 4.1.0, on pointycastle 4.0.0 |
| Curve25519 key exchange, Ed25519 | RFC 7748, RFC 8032, RFC 8731, RFC 8709 | SSH: dartssh2 4.1.0, on pinenacl 0.6.0. Generating SSH keys: cryptography 2.9.0 |
| ECDH and ECDSA over NIST P-256, P-384 and P-521; RSA signatures; Diffie-Hellman | FIPS 186-5; RFC 5656, RFC 8332, RFC 4419, RFC 8268 | SSH: dartssh2 4.1.0, on pointycastle 4.0.0. Generating ECDSA and RSA SSH keys: pointycastle 4.0.0 |
| The SSH protocol | RFC 4251 to 4254 and the RFCs above; OpenSSH's published specifications for its aes-gcm, chacha20-poly1305 and encrypt-then-MAC variants | dartssh2 4.1.0 |
| TLS | IETF TLS (RFC 8446 and earlier versions) | The Dart runtime's built-in BoringSSL; in the web view, the platform's own TLS |
| Unicode NFC normalization | Unicode Standard Annex #15 | Passphrases for backups and sync: unorm_dart 0.3.2 |
Every random key, salt, nonce and ID in the files described on this page comes from the operating system's secure random number generator, through Dart's Random.secure().
Notation
|means the byte strings are joined end to end.- Text written in
codeinside a key derivation or associated data means its ASCII bytes, with no terminator or length prefix. - Lengths are in bytes unless they say otherwise. KiB is 1,024 bytes and MiB is 1,048,576 bytes.
Questions about this page, or a security problem to report: see Support.