Nectenda

What Nectenda's server can and cannot see

Nectenda is end-to-end encrypted: the server stores an append-only log of ciphertext it has no key for. This document says exactly what that does and does not cover, because a privacy claim you cannot check is worth very little.

Every claim below names the code that implements it. If a claim and the code ever disagree, the code is right and this document is a bug.

Scope. This describes the cryptography and what the server holds. It is not a promise about a particular deployment's logging, retention or jurisdiction; those belong in a privacy policy and are not verifiable from code by anyone.


The short version

Server sees
Note content No — AES-256-GCM ciphertext only
Note and folder paths No — document ids are HMACs of the path
Attachment content No — sealed in a chunked AEAD envelope
Attachment filenames No — they live inside the encrypted listing
Your password No — never sent; not even in hashed-and-then-hashed form
Your private key No — stored wrapped, unwrappable only by your password
Folder display names No — sealed under the folder's content key
Who shares a folder with whom Yes
Sizes, timing, device and account records Yes

Why you do not have to trust the server

The keys never reach it.

Your password never leaves the device. It is stretched with PBKDF2-SHA256 at 600,000 iterations into a master key (deriveMasterKey, packages/shared/src/crypto.ts). The master key is never sent and never stored. What the server receives at login is a second, independent derivation, deriveAuthHash, which cannot be used to derive the encryption key — and the server bcrypts even that before storing it, because it is still password-equivalent for logging in.

Your identity key is wrapped before it is uploaded. A P-256 ECDH keypair is generated on device; the private half is encrypted under a key derived from the master key (deriveEncKey) and only then stored server-side as users.wrapped_private_key. The server holds a blob it cannot open.

Folder keys are wrapped to each member individually. Sharing a folder means sealing its content key to the recipient's public key using ECIES over P-256 with a fresh ephemeral keypair per wrap (wrapSecret / wrapFolderKey). The server stores the results in folder_keys — ciphertext, one row per member. It never handles an unwrapped folder key.

Folder names are sealed too. A folder called "Redundancy consultation — legal" said as much as the notes inside it, so shared_folders.name holds ciphertext under the folder's content key, with name_key_id recording which generation sealed it (sealFolderName in packages/plugin/src/folder-crypto.ts). The column is not nullable and the create route refuses an unsealed name: there is no such thing as a folder whose name the server can read.

Document ids are HMACs, not paths. A document is addressed as {folderId}/{HMAC-SHA256(nameKey, relativePath)} truncated to 16 bytes (deriveDocId). The server cannot reverse it and therefore never learns your folder structure or note titles. The name key is wrapped to members like any other folder secret, and is deliberately separate from the content key so that rotating content keys does not rewrite every id in the log.

Content is encrypted before it is pushed. Each Yjs update is sealed with AES-256-GCM, IV prepended, 28 bytes of overhead (encrypt). Attachments use a chunked envelope whose additional authenticated data binds blobId ‖ chunkIndex ‖ totalChunks ‖ algo ‖ codec, so a reordered, truncated or substituted chunk is rejected rather than decrypted (packages/shared/src/blob-cipher.ts).

The attack this does not stop by itself

Encrypting content protects you from a server that reads. It does not, on its own, protect you from a server that actively interferes.

When you add a collaborator, your client asks the server for their public key and wraps the folder key to it. A malicious server could return its own public key instead. You would be sharing with the operator, and everything would look normal.

The defence is fingerprint comparison, and it requires you to act. The folder members screen shows a fingerprint for every member and for you (publicKeyFingerprint, used in packages/plugin/src/main.ts). Compare them with your collaborators through any channel that is not this server — in person, a phone call, a different messenger. If they match, no key was substituted.

This is the same mechanism as Signal's safety numbers and Threema's QR verification, and it carries the same caveat: it only works if somebody checks. Until then the guarantee holds against an operator who reads, not one who interferes.

What is not encrypted

Stated plainly, because a security document that only lists strengths is marketing.

Forgetting your password

There is a way back, and it is the recovery key you were shown once at registration. Nothing else works: your password is not stored, and it is not merely checked but used — it derives the key that unwraps everything — so no administrator, and no amount of access to the server, can reset it for you.

Asking to recover discloses nothing until you prove you hold the key. Requesting recovery parameters for a username returns only derivation parameters, and returns plausible ones even for accounts that do not exist, so it cannot be used to find out who has an account here. The wrapped key itself is released only after the server has verified a proof it cannot forge, and failed attempts are counted against the account rather than the network address they arrive from.

Recovering does not change your identity key, so every folder shared with you stays readable afterwards. It does issue a new recovery key, because the old one no longer opens anything — save it as you saved the first.

Traffic analysis is real. Sizes and timing alone can reveal that a document is being actively edited, roughly how large it is, and who was connected at the time. End-to-end encryption does not hide that, here or anywhere else, and no amount of auditing the client will tell you what a server chooses to retain.

If that metadata is what you need to protect, the answer is not cryptography — it is running the server yourself.

What we do not claim

Checking this yourself

The client is the whole trust anchor, and unusually for an encrypted product, you can read the exact code that runs. Obsidian plugins ship as JavaScript: main.js is on your disk, in your vault, at .obsidian/plugins/nectenda/main.js. There is no compiled binary and no reproducible-build problem to reason about — the file that executes is the file you can inspect.

This is also why there is no Nectenda web app, and will not be one. A web page is delivered fresh by a server on every visit, and can be delivered differently to different people, so "read the code that runs" is not a promise a browser-based product can keep. Everything that touches your password, your master key, your identity key or a folder key happens in the plugin on your disk. Anything of ours you reach in a browser — a marketing page, a checkout, an invoice — never sees any of them, and cannot create or recover an account.

That boundary is a deliberate limit on the product, not an accident of what has been built yet. It is the reason the claim above holds.

Worth checking, in rough order of value:

  1. That plaintext never leaves. Every push goes through encrypt before it reaches the socket — see packages/plugin/src/multiplexed-provider.ts.
  2. That paths are not sent. Document names are derived by deriveDocId; grep the traffic for a note title and you will not find one.
  3. That the server's copy is unreadable. Attach to the server's database and look at doc_updates.payload. It is opaque without a folder key.
  4. That fingerprints match what your collaborator sees.

That the file you installed is the source you read

The above assumes the main.js in your vault corresponds to the published source. You do not have to assume it. Every release carries the SHA-256 of its main.js, and the source it was built from is the repository the release lives in, at that tag:

git clone https://github.com/Nectenda/nectenda-plugin && cd nectenda-plugin
git checkout <the release tag>
node scripts/verify-build.mjs --built ~/vault/.obsidian/plugins/nectenda/main.js

It rebuilds and compares byte for byte. On a difference it reports the offset and prints both sides around it, because "they do not match" does not distinguish a differing esbuild version from a differing cipher — and those deserve very different reactions.

The licence does not restrict any of this. The plugin is source-available under PolyForm Shield 1.0.0: reading, auditing, building and comparing are expressly permitted. What is withheld is the right to build a competing product, which is not something verification requires. If anything the check matters more now than it did under MIT — having narrowed what you may do with the code, the claim rests entirely on what you can check.


Sources of comparison

Relay, the closest competitor, publishes an equivalent page and states:

"Relay isn't end-to-end encrypted, we have the technical ability to access synced content if required."

The difference is structural rather than a matter of degree, and it is the reason this document can be specific where a trust promise cannot be. Quoted from their documentation on 1 September 2026; check it yourself rather than taking our word for what a competitor says.