Skip to content

Client-Side Encryption

By default PrefsDB stores your data encrypted on its servers, using a key that PrefsDB holds. That protects your data if a disk or a backup is stolen. It does not stop PrefsDB itself from reading it.

Client-side encryption is an optional extra for data that PrefsDB should not be able to read at all. Your app scrambles each value with a key that only you have before it leaves the browser, and unscrambles it when it comes back. PrefsDB only ever stores the scrambled version.

It is off unless you turn it on, and everything works exactly as before if you don't. Turning it on is a trade, so please read what you give up first.

Turn it on

Make a key once, and keep it safe:

const key = PrefsDB.generateEncryptionKey();   // a long random string
// Save it in your user's password manager or your secrets store.
// PrefsDB never sees it and cannot get it back for you.

Then use it when you get your PrefsDB key, or on an instance you already have:

const db = await PrefsDB.getkey("my-app", ["a", "b"], { email, encryption: { key } });

// or, on an existing instance:
await db.enableEncryption({ key });

await db.key("diary").write("Dear diary...");         // stored scrambled
const { value } = await db.key("diary").read();       // "Dear diary..."

Prefer something your user can type instead of a generated key? Use a passphrase and a salt (a string that stays the same for that user, such as their id):

await db.enableEncryption({ passphrase: "a long passphrase only the user knows", salt: "user-12345" });

Do not use the same words as your getkey() seed. That seed is only lightly scrambled before it is sent, because it just has to identify a key. An encryption passphrase must be something PrefsDB never receives in any form.

Options:

Option What it does
key A key from generateEncryptionKey().
passphrase and salt A key worked out from words you type (takes a moment, once). Use key or passphrase, not both.
deterministic Off by default. On: the same value always scrambles to the same text. That keeps lists free of duplicates and allows exact-match filters, but PrefsDB can then see which of your values are the same.
strict Off by default. On: anything stored without encryption is treated as an error instead of being returned. Useful if you don't trust what comes back.

Encryption happens in your page or script, so it needs a secure (https:) page. In Node.js 18, also pass crypto: require("node:crypto").webcrypto; Node 19 and newer need nothing.

What gets encrypted

Encrypted Still visible to PrefsDB
Every value you write Key names, and the names of your projects, domains, lists and maps
Every file you upload File names
How big things are, when they were written, how often you call

What you give up

  • A lost key means lost data. PrefsDB cannot recover it, and neither can support. There is no reset.
  • Your dashboard shows scrambled text, unless you unlock it. "My Data" shows "Value encrypted" instead of the value. Click it, enter your key the first time (later clicks reuse it), and that value appears in place: the key stays in that browser tab and is never sent to PrefsDB, and it is forgotten after 15 minutes of not using it. The page is served by PrefsDB, so this protects your stored data, not against a modified page. Previewing or downloading an encrypted file works the same way: it tells you the file is encrypted and asks for the key if none is held (or lets you download the encrypted file as it is), then decrypts it in your browser: small files open in the page, larger text files show their first part, and a download is saved to your device straight after decrypting (Chrome, Edge and similar browsers stream it to disk; other browsers decrypt in memory, up to about 256 MB). A decrypted download is saved as a normal, unencrypted file. On the review page, "Download All Data" gives scrambled data as stored; with the key unlocked, "Decrypt and download" decrypts it in your browser and saves a zip that is no longer encrypted (in browsers that can't stream to disk, files over about 256 MB are left out and only the values are decrypted).
  • No pattern searches. match("%word%") cannot work. An exact-value match("word") works only with deterministic: true.
  • Lists can hold duplicates unless you use deterministic: true.
  • No share links or previews for files. A link would show scrambled bytes. putObject() doesn't return a share link, getObjectUrl() is refused, and you show a file by downloading it with getObject().
  • Larger values. Scrambling adds about a third to a value's size. The value size limit applies to the scrambled size, so about 700 KB of your text fits on the free plan's 1 MB (3 MB on Plus, 6 MB on Pro).
  • A value only unscrambles where it was written. Each value is tied to its domain, subdomain, project, and the list or map it is in (not to the key name). Read it back with the same domain()/project()/list()/map() you wrote it with (capital letters don't matter), and if someone copies it to another place it simply stops unscrambling there, instead of showing up as your data.
  • Old data stays as it was. Only what you write after turning it on is scrambled. To protect existing data, read it and write it back.
  • Changing the key means rewriting the data. Read with the old key, write with the new one.
  • Every device needs the key. Give users a way to bring it (or the passphrase) along.
  • PrefsDB can still tamper in small ways. It cannot read or change a value without you noticing, but it could hand back an older version of a value, or delete something.
  • Not covered: real-time channels. Messages sent through channels are not encrypted by this.
  • It doesn't protect against bad code on your own page. Anything running in your page can use the key.

Keeping it safe in your app

Encryption protects what PrefsDB stores. It cannot protect against someone running code inside your page (a cross-site scripting, or XSS, hole): that code can use the key just as your own code does. So:

  • Never write values into your page as HTML. Show them with textContent, or escape them first. Anyone who has your API key can store text of their own, and unless you use strict: true the client hands back a value that isn't encrypted as it is, so it may not be yours.
  • Keep the key out of localStorage and sessionStorage. Any script on your site can read those. Ask for a passphrase and keep the result in memory only, and never put a key in code you ship to every visitor.
  • Use a strong key. PrefsDB.generateEncryptionKey() makes one. If you use a passphrase, add a salt that is unique to each user, and choose a long passphrase: anyone holding the stored data can guess offline. Don't reuse the seed you pass to getkey().
  • Treat the API key as a secret too. With it, someone can overwrite or delete your data even without the encryption key.
  • Know which scripts you load. The client library handles your key. If you load it from PrefsDB, PrefsDB controls that code; to remove that trust, host a copy yourself and pin it with a Subresource Integrity (integrity=) attribute. The same goes for any third-party script on the page.
  • Decrypted downloads are plain files. Keep them somewhere safe, and delete them when you are done.

Reading things back

A value that cannot be unscrambled (wrong key, someone altered it, or it was read from a different place than it was written) never comes back as junk. When it was written with another key, the answer says so (wrongKey: true). A single read answers with success: false and error: "DECRYPT_FAILED". In a bulk read the affected values are null and the answer says how many failed in decryptFailed.

If you pass onsuccess to read(), it receives the answer before unscrambling. Use the Promise that read() returns instead.

Using the HTTP API directly

The server doesn't do any of this and doesn't need to know about it: it stores whatever text or file you send. If you call the HTTP API yourself, encrypt before you send and decrypt after you read; the format is the same one the JavaScript client uses, described in the developer notes.